---
title: 'Common Interface Standards'
source: 'https://academia.sh/en/courses/php-object-model/common-interface-standards'
course: 'Object-Oriented PHP and Standards'
language: en
updated: '2026-10-21T05:03:35+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that a consumer bound to a common interface is not edited when the implementation changes'
    - 'Shows that the change is absorbed at the seam in the setup file'
    - 'Shows with an implicit measurement that an interface checks the signature but not the meaning'
---

# Common Interface Standards

Common interfaces such as PSR-3; the consumer bound to the interface while the implementation changes in the setup file; an interface checking the signature but not the meaning; an implementation that leaves placeholders unfilled silently changing the output.

The Autoloading lesson gathered in one place where a class comes from. This lesson turns to the inside of the class: if the consumer is bound to a **common interface** and the implementation changes, **where does this change reach?** The same question, the same driver, the same reach measure.

A **common interface standard** is many libraries and applications agreeing on the same signature. **PSR-3**, the PHP community's recommendation for logging, is the well-known example: you call a logger with a message and a context array, and the `{key}` placeholders inside the message are replaced with the context values. This lesson does not use the whole of PSR-3 but a small form of the same idea: a `Logger` interface with a single method, and placeholders such as `{number}`.

## What a Standards Recommendation Means

PSR is the abbreviation of the **standards recommendations** the PHP community publishes, and it is cited by number: PSR-3 defines logging, PSR-4 defines autoloading. The word "recommendation" in the name is literal: the language does not check whether a recommendation is followed. A library adopting PSR-3 is that library's own decision. Its value arises through use: if two libraries adopt the same interface, they can work in the same application without knowing about each other.

## Who Knows What

The real job of a common interface is to bring together two pieces of code that do not know each other. In the driver below, a **library** takes a payment and wants to record it; an **application** decides where the record goes. The library knows only the `Logger` interface. The application knows the interface and writes two implementations. The two meet only at the interface: the library file contains the name of no implementation.

```php
<?php
// whoknows.php — the library knows the interface, the application picks the implementation
mkdir('measure/whoknows', 0777, true);
file_put_contents('measure/whoknows/library.php', <<<'PHP'
    <?php
    interface Logger
    {
        public function log(string $message): void;
    }
    class PaymentService
    {
        public function __construct(private Logger $logger) {}
        public function receive(int $amount): void { $this->logger->log("payment received: $amount"); }
    }
    PHP);
file_put_contents('measure/whoknows/app.php', <<<'PHP'
    <?php
    class ScreenLogger implements Logger
    {
        public function log(string $message): void { echo $message, "\n"; }
    }
    class UppercaseLogger implements Logger
    {
        public function log(string $message): void { echo strtoupper($message), "\n"; }
    }
    PHP);

$library = file_get_contents('measure/whoknows/library.php');
$mentions = str_contains($library, 'Screen') || str_contains($library, 'Uppercase');
echo 'library mentions an implementation: ', $mentions ? 'yes' : 'no', "\n";

require 'measure/whoknows/library.php';
require 'measure/whoknows/app.php';
(new PaymentService(new ScreenLogger()))->receive(50);
(new PaymentService(new UppercaseLogger()))->receive(50);
```

```
library mentions an implementation: no
payment received: 50
PAYMENT RECEIVED: 50
```

The first line shows that the library is unaware of the implementations. In the next two lines the same `PaymentService` worked with two different loggers, and its output changed accordingly. The library did not change by a single line. This is the reason for what the previous section measured: the seam is not inside the library but in a place such as `setup.php`, which selects the implementation. The interface draws the boundary, and the code on the two sides of the boundary stays closed to each other's changes.

## When No Logger Is Given

If the library is bound to the interface, the case where the application gives no implementation must also be considered. The common solution is to use an implementation that does nothing as the default; PSR-3 also recommends a separate empty logger class for this. This way the library does not have to check for `null` on every call, and an application that wants no logging binds nothing.

```php
<?php
// empty.php — without a logger the library works with the quiet implementation
interface Logger
{
    public function log(string $message): void;
}
class QuietLogger implements Logger
{
    public function log(string $message): void {}
}
class ScreenLogger implements Logger
{
    public function log(string $message): void { echo $message, "\n"; }
}
class ReportService
{
    private Logger $logger;

    public function __construct(?Logger $logger = null)
    {
        $this->logger = $logger ?? new QuietLogger();
    }

    public function generate(): int
    {
        $this->logger->log('report generated');
        return 3;
    }
}

$quiet = (new ReportService())->generate();
echo "without logger: $quiet\n";
$onScreen = (new ReportService(new ScreenLogger()))->generate();
echo "on screen: $onScreen\n";
```

```
without logger: 3
report generated
on screen: 3
```

Both calls returned the same value; only the second wrote a log line. Logging does not interfere with the real job of the library. For this reason the logging interface is often distributed not with the library but in a separate and very small package: libraries bind only to the interface, and applications bind to the package that implements it. Dependencies flow toward the interface, not toward each other.

## An Interface Is a Boundary

The Interfaces lesson (M08/K03) showed that an interface is a boundary, not a promise: PHP checks the signature, not the behavior. This lesson ties that distinction to a measurement. First the case where the interface works well, then the case where it does not check.

**SP3.** The lesson uses the `Logger` interface not as the whole of PSR-3 but reduced to its single `log` method. Level names, the exception context and `Stringable` messages are not measured.

## PSR-3: Levels and the Context Array

The PSR-3 interface defines a method for each level and adds a general `log` method. There are eight levels, from most severe to least: `emergency`, `alert`, `critical`, `error`, `warning`, `notice`, `info`, `debug`. The message is a string and may carry placeholders of the form `{key}`; the second argument, the context array, supplies the values of these placeholders. In the context, the `exception` key is reserved for an exception object. The standard does not say which level is written and which is discarded; that is an implementation decision.

The driver below models a small implementation: the severity order is the array order, and a threshold level discards the records below the threshold, that is, the lighter ones. The same four records are written with two different thresholds.

```php
<?php
// levels.php — eight levels, a threshold and the context array
$levels = ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'];

function emit(array $levels, string $threshold, string $level, string $message, array $context): void
{
    if (array_search($level, $levels) > array_search($threshold, $levels)) {
        return;
    }
    $replacements = [];
    foreach ($context as $key => $value) {
        $replacements['{' . $key . '}'] = $value;
    }
    echo str_pad($level, 10), strtr($message, $replacements), "\n";
}

$records = [
    ['critical', 'database {server} is not responding', ['server' => 'db1']],
    ['warning', 'order {number} is late', ['number' => 42]],
    ['info', 'user {person} logged in', ['person' => 'alice']],
    ['debug', 'query time {time} ms', ['time' => 12]],
];
foreach (['warning', 'debug'] as $threshold) {
    echo "-- threshold: $threshold\n";
    foreach ($records as [$level, $message, $context]) {
        emit($levels, $threshold, $level, $message, $context);
    }
}
```

```
-- threshold: warning
critical  database db1 is not responding
warning   order 42 is late
-- threshold: debug
critical  database db1 is not responding
warning   order 42 is late
info      user alice logged in
debug     query time 12 ms
```

At the `warning` threshold only the `critical` and `warning` records were written; `info` and `debug` were discarded. At the `debug` threshold all four were written. In both cases the messages came out with the placeholders filled: `db1` in place of `{server}`, `42` in place of `{number}`. The benefit of writing the message once with a fixed pattern and carrying the variable part in the context is that different implementations can process the same pattern: one can produce plain text, another a structured record.

## The Measurement Setup

`reach.php` is the same as in the earlier lessons: it builds the same program before and after a change and runs both. It asks for the reach in order: if the exit code is not 0 the reach is **breaking**, if the output differs it is **implicit**, if the edited file is in the seam list it is **at the seam**, and if none of these holds it is **in place**. The earlier version must exit with 0, and the consumer file `main.php` is not edited.

```php
<?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;
}
```

## Changing the Implementation: at the Seam

In the `common-interface` measurement, the `Order` class knows only the `Logger` interface and has it write an order message. `setup.php` selects which implementation is used; `main.php` takes it with `require` and gives it to `Order`. In the baseline, `ScreenLogger` prints the message straight to the screen. The change is a single file: `setup.php` now returns a `BufferLogger` that collects the messages in a buffer and prints them all at the end of the program. The fourth argument is the seam list and names `setup.php` as the seam.

`Order` does not fill the placeholder itself. The `interpolate()` function that fills the `{number}` placeholder sits in the same file as the interface, and both implementations call it; `Order` knows only the `log` method.

```php
<?php
// setup.php change — the implementation changes in the setup file
require_once 'reach.php';

// 10. The implementation of a common logging interface is replaced
$reaches[] = measure('common-interface', [
    'logger.php' => <<<'PHP'
        <?php
        interface Logger
        {
            public function log(string $message, array $context = []): void;
        }
        function interpolate(string $message, array $context): string
        {
            $replacements = [];
            foreach ($context as $key => $value) {
                $replacements['{' . $key . '}'] = $value;
            }
            return strtr($message, $replacements);
        }
        class ScreenLogger implements Logger
        {
            public function log(string $message, array $context = []): void
            {
                echo interpolate($message, $context), "\n";
            }
        }
        PHP,
    'setup.php' => <<<'PHP'
        <?php
        return new ScreenLogger();
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'logger.php';
        class Order
        {
            public function __construct(private Logger $logger) {}
            public function place(int $number): void { $this->logger->log('order {number} received', ['number' => $number]); }
        }
        (new Order(require 'setup.php'))->place(7);
        PHP,
], [
    'setup.php' => <<<'PHP'
        <?php
        class BufferLogger implements Logger
        {
            private array $lines = [];
            public function log(string $message, array $context = []): void
            {
                $this->lines[] = interpolate($message, $context);
            }
            public function __destruct() { echo implode("\n", $this->lines), "\n"; }
        }
        return new BufferLogger();
        PHP,
], ['setup.php']);

echo run('measure/common-interface/before')[1], "\n";
echo run('measure/common-interface/after')[1], "\n";
$names = array_keys(read_files('measure/common-interface/after'));
sort($names);
echo implode(', ', $names), "\n";
```

```
common-interface   files=1  exit=0   output same    -> at-seam
order 7 received
order 7 received
logger.php, main.php, setup.php
```

The first line is the result of the measurement: one file changed, the exit code stayed 0, the output stayed the same, and because the changed file is in the seam list, the reach is **at the seam**. The next two lines are the output of the two versions of the program: the implementation that writes to the screen and the implementation that buffers gave the same `order 7 received` line. The last line is the sorted list of files in the directory `measure/common-interface/after`: `main.php` is in the list and was never edited during the measurement.

The consequence is this: because the consumer is bound to the interface, `Order` and `main.php` stayed in place when the implementation changed. The change was absorbed at the point `setup.php`. Going from the implementation that prints to the one that buffers was a one-file job, and the output stayed the same.

## The Bounding Measurement

An interface checks the signature, not the meaning. One more implementation is applied to the same baseline: its `log` method satisfies the interface completely as a signature, but it does not use the context and prints the message without filling the placeholder. The driver is a separate measurement and runs under `measure/no-placeholder`; the baseline is the baseline of the previous measurement.

```php
<?php
// no-placeholder.php — an implementation that meets the signature but not the meaning
require_once 'reach.php';

$base = read_files('measure/common-interface/before');
measure('no-placeholder', $base, [
    'setup.php' => <<<'PHP'
        <?php
        class PlainLogger implements Logger
        {
            public function log(string $message, array $context = []): void
            {
                echo $message, "\n";
            }
        }
        return new PlainLogger();
        PHP,
], ['setup.php']);

echo run('measure/no-placeholder/before')[1], ' -> ', run('measure/no-placeholder/after')[1], "\n";
```

```
no-placeholder     files=1  exit=0   output changed -> implicit
order 7 received -> order {number} received
```

The reach became **implicit**: one file changed and the exit code stayed 0, but the output went from `order 7 received` to `order {number} received`. `PlainLogger` meets the interface completely as a signature; PHP said nothing and the program did not crash. Although the changed file is in the seam list, the reach counts as **implicit** because the output difference comes before the seam in the order of the checks. The interface protected the boundary, but not the meaning. The promise to fill the placeholder stands only in the documentation and in the text of the common standard; something has to check it, a measurement or a test.

## Summary

- The reach of this lesson is **at the seam**. In the `common-interface` observation the implementation changed in the file `setup.php`, `Order` and `main.php` were not edited, and the output stayed `order 7 received`.
- A common interface (such as PSR-3) separates the consumer from the implementation; the change is absorbed at the seam in the setup file.
- The bounding measurement `no-placeholder` is **implicit**: an implementation that meets the signature but does not fill the `{number}` placeholder exited with 0, and the output became `order {number} received`.
- The library knows only the interface and the application selects the implementation; PSR-3 defines eight levels and a context array with `{key}` placeholders, and the implementation decides which level is written.
- An interface checks the signature, not the meaning; the meaning needs a separate measurement.

## Next Step

The setup file selected the implementation. But if a dependency is code published by someone else and that code is updated, how many files are edited? The Dependency Manager lesson measures this.
