Lesson 03 / 12
Interfaces
A class that implements an interface failing to declare when a method is added to the interface, the interface as a type and instanceof, and the same addition staying in place in an abstract class with a body.
Published Updated
In this lesson
- Defines an interface, implements several interfaces in one class and uses an interface as a parameter type
- Queries with instanceof which interface an object implements
- Measures with the exit code that adding a method to an interface stops the implementing class
- Shows that the same addition stays in place in an abstract class with a body, and what that costs
Contents
In the Inheritance and Abstract Classes lesson, adding a new abstract method to an abstract class stopped the subclass, and the reason was that the contract had no body. An interface is the plain form of that contract: it carries only signatures and no body at all. The question is what the classes that implement a contract experience when a method is added to it.
Interfaces and Implementation
An interface declares which methods a class must offer and writes none of them. A class that applies an interface declares it with implements and must write all the methods; this is called implementation. A class can implement several interfaces, separated by commas.
<?php // Payable.php — an interface that carries only a signature interface Payable { public function amount(): int; }
<?php // Printable.php — a second interface interface Printable { public function text(): string; }
<?php // Invoice.php — a class that implements two interfaces at once class Invoice implements Payable, Printable { public function amount(): int { return 120; } public function text(): string { return 'invoice'; } }
<?php // Salary.php — a class that implements only Payable class Salary implements Payable { public function amount(): int { return 300; } }
An interface is also used as a type. The function below expects Payable and accepts an object of any class that implements this interface. The instanceof operator asks whether an object implements the given interface.
<?php // interface_usage.php — an interface as a parameter type and with instanceof require 'Payable.php'; require 'Printable.php'; require 'Invoice.php'; require 'Salary.php'; function totalAmount(Payable ...$items): int { $total = 0; foreach ($items as $item) { $total += $item->amount(); } return $total; } echo totalAmount(new Invoice(), new Salary()), "\n"; var_dump(new Invoice() instanceof Printable); var_dump(new Salary() instanceof Printable);
420 bool(true) bool(false)
OM10. totalAmount() took objects of two different classes because both implemented Payable: 120 + 300. Invoice conforms to two interfaces, Salary to only one; instanceof reported that difference as true and false. An interface checks the signature, not the meaning: amount() could have returned a negative number and the language would not have made a sound. The Standard Library and Streams asked who gives the guarantee of a behavior; here the interface is not a promise but a boundary. Measuring meaning beyond the signature is left to the Common Interface Standards lesson.
Interface Constants and Interfaces Extending Interfaces
An interface carries not only signatures but constants too. An interface constant is read under the same name in every implementing class. An interface can extend another interface with extends; in that case the class that implements the extending interface must write all the signatures of both interfaces. The single inheritance rule of classes does not apply to interfaces: an interface can extend several interfaces.
<?php // Taxed.php — an interface that extends Payable and carries a constant require_once 'Payable.php'; interface Taxed extends Payable { public const RATE = 20; public function tax(): int; }
<?php // Sale.php — a class that implements Taxed, writing the signatures of both interfaces require_once 'Taxed.php'; class Sale implements Taxed { public function amount(): int { return 200; } public function tax(): int { return intdiv($this->amount() * self::RATE, 100); } }
<?php // sale_usage.php — the extending interface, the constant and the parent interface type require 'Sale.php'; $sale = new Sale(); echo $sale->amount(), ' ', $sale->tax(), ' ', Taxed::RATE, "\n"; var_dump($sale instanceof Payable);
200 40 20 bool(true)
Sale says it implements only Taxed, yet instanceof Payable also gave true: an extending interface counts as the type of its parent interface. The constant is read both through the interface’s name as Taxed::RATE and from inside the implementing class as self::RATE.
The Interface as a Type, and Not Instantiable
An interface cannot be instantiated like a class, because it has no body. An object that does not carry the same contract also cannot enter a parameter typed with an interface. Both appear as an error at run time; the first throws Error, the second TypeError.
<?php // type_error.php — an interface cannot be instantiated, an object without the contract cannot enter the parameter require 'Sale.php'; function pay(Payable $item): int { return $item->amount(); } echo pay(new Sale()), "\n"; try { pay(new stdClass()); } catch (TypeError $e) { echo get_class($e), "\n"; } try { $item = new Payable(); } catch (Error $e) { echo get_class($e), "\n"; }
200 TypeError Error
pay() depends not on a class but on a behavior: Sale, Invoice or another class written tomorrow will do as long as it implements Payable. This independence is the real benefit of an interface, because the calling code does not know the implementation. On the other hand, stdClass, which does not carry the contract, is caught by the type check and causes a TypeError.
The Implementer’s Obligation
A class that declares an interface must write all of the interface’s methods and make all of them public. If one is missing or its visibility is narrowed, the class cannot be declared. The script below tries these two cases in separate files.
# obligation.sh — a missing method and a narrowed visibility make the class impossible to declare cat > Missing.php <<'PHP' <?php interface Payable { public function amount(): int; } class Blank implements Payable {} PHP cat > Narrow.php <<'PHP' <?php interface Payable { public function amount(): int; } class Hidden implements Payable { private function amount(): int { return 1; } } PHP php -d display_errors=0 Missing.php; echo "missing exit=$?" php -d display_errors=0 Narrow.php; echo "narrow exit=$?"
missing exit=255 narrow exit=255
The restriction exists not to protect the class but to protect the consumer: the amount() method of an object that enters a parameter typed with an interface must always be callable. This guarantee makes a substitution like the one below safe.
<?php // fake.php — a fake implementation that carries the contract takes the place of the real one require 'Payable.php'; require 'Printable.php'; require 'Invoice.php'; class FakeItem implements Payable { public function amount(): int { return 1; } } $items = [new Invoice(), new FakeItem()]; echo array_sum(array_map(fn (Payable $item): int => $item->amount(), $items)), "\n";
121
The function inside array_map() knows only Payable; a FakeItem written for testing passed through the same code together with a real Invoice. This is what an interface offers in testing: it becomes possible to run the code without tying it to a heavy dependency such as a real payment or a real file. Passing a dependency in from outside, covered in the course closing, is built on this benefit and would not give the same flexibility without an interface.
The Measurement Core
This lesson’s measurement also uses the file reach.php; the core explained in Classes and Objects is rewritten in this working directory.
<?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 Interface
The change to be measured is adding a second signature to the interface above: currency(). The implementing class Invoice carries only the amount() method. The consumer main.php declares the class and prints the value of amount(); the file is not edited.
<?php // interface.php — a new method is added to the interface require_once 'reach.php'; measure('interface-method', [ 'payable.php' => <<<'PHP' <?php interface Payable { public function amount(): int; } PHP, 'main.php' => <<<'PHP' <?php require 'payable.php'; class Invoice implements Payable { public function amount(): int { return 120; } } echo (new Invoice())->amount(), "\n"; PHP, ], [ 'payable.php' => <<<'PHP' <?php interface Payable { public function amount(): int; public function currency(): string; } PHP, ]); foreach (['before', 'after'] as $stage) { [$code, $output] = run("measure/interface-method/$stage"); echo $stage, ' exit=', $code, ' output=', $output, "\n"; }
interface-method files=1 exit=255 output changed -> breaking before exit=0 output=120 after exit=255 output=
OM11. One file was edited and the consumer stopped: because Invoice no longer carried the currency() method, it could not be declared, exit 255. The earlier run had given 120. The error is not in the consumer’s own code but in the implementing class; when an interface’s signature changes, every class that implements it has to meet the contract again. The reach of this lesson is breaking. measure() does not print the error message, it sees only the exit code.
Abstract Class or Interface
Both can declare signatures without a body, but they say different things to a class. An interface says “you offer this behavior” and builds a common name among unrelated classes; it cannot carry state or a body; it can declare constants and, since PHP 8.4, a property contract. An abstract class says “you are from this family”; it can carry shared properties and methods with a body, but it spends the class’s single inheritance right.
Three questions steer the choice. Are the implementers related to each other, or do they merely offer the same behavior? Will a shared body or shared state be passed around? Will the contract grow over time? If you answer “only the same behavior” to the first and “no” to the second, an interface is right. If you want shared state or a body, you need an abstract class; a class both extending an abstract class and implementing several interfaces is a common combination. If you answer “yes” to the third, the measurement below shows the cost of this choice: as an interface grows, every one of its implementers stops, while as an abstract class with a body grows, they do not. For this reason, choosing an abstract class for a behavior whose contract will change often and an interface for a boundary that will stay fixed for a long time is the writer’s decision about who will pay for the change.
The Bounding Measurement
Is the source of the break the concept of an interface, or the added method having no body? Adding the same method with its body to an abstract class separates the two. In this measurement the contract is an abstract class, Invoice extends it, and the added currency() method carries a body.
<?php // with_body.php — the same extra method, this time with its body in an abstract class require_once 'reach.php'; measure('abstract-body', [ 'payable.php' => <<<'PHP' <?php abstract class Payable { abstract public function amount(): int; } PHP, 'main.php' => <<<'PHP' <?php require 'payable.php'; class Invoice extends Payable { public function amount(): int { return 120; } } echo (new Invoice())->amount(), "\n"; PHP, ], [ 'payable.php' => <<<'PHP' <?php abstract class Payable { abstract public function amount(): int; public function currency(): string { return 'USD'; } } PHP, ]); foreach (['before', 'after'] as $stage) { [$code, $output] = run("measure/abstract-body/$stage"); echo $stage, ' exit=', $code, ' output=', $output, "\n"; }
abstract-body files=1 exit=0 output same -> in-place before exit=0 output=120 after exit=0 output=120
OM12. The same method, the same name, the same consumer; this time the class stayed in place: exit 0, output 120. Because the added method had a body, Invoice did not need to write anything; the method was inherited. So what caused the break was not the word interface but the contract without a body. This solution has a cost: a class can extend only one class. The script below writes a declaration that lists two parent classes separated by a comma and prints the exit code.
# single.sh — a class cannot extend two classes cat > Double.php <<'PHP' <?php abstract class Payable {} abstract class Printable {} class Invoice extends Payable, Printable {} PHP php -d display_errors=0 Double.php; echo "exit=$?"
exit=255
A class can implement as many interfaces as it likes but can extend only one class. An abstract class with a body keeps the change in place, but it spends the class’s single inheritance right. An interface does not spend that right; in return it imposes every added method on its implementers.
Summary
- An interface carries only signatures, a class can implement as many interfaces as it likes with
implements, and an interface is used as a parameter type;instanceofsays which interface is implemented. - An interface checks the signature, not the meaning.
- When the
currency()signature was added to the interface,Invoicecould not be declared, with exit 255 instead of120; the reach of this lesson is breaking. - When the same method was added to an abstract class with its body, the class stayed in place and the output continued as
120. - The cost of an abstract class with a body is single inheritance: a class cannot extend two classes, exit 255.
Next Step
An abstract class could carry a body but left a class only one inheritance right. The Traits lesson asks about another way of sharing a body: if two traits bring a method under the same name, what does the class that uses them experience?
To keep your progress and take notes, Log in
My notes
Log in to take notes.