---
title: 'Object-Oriented PHP and Standards'
source: 'https://academia.sh/en/courses/php-object-model'
language: en
updated: '2026-10-21T05:03:34+00:00'
license: 'CC BY-SA 4.0'
lessons: 12
outcomes:
    - 'Measures with a before/after run whether a change stays in place, is absorbed at the seam, breaks its users or implicitly changes their result'
    - 'Organises code with classes, interfaces and traits, and uses visibility as a boundary'
    - 'Builds a directory layout that follows the namespace and autoloading standards'
    - 'Explains how a dependency declaration, a version constraint and a lock file decide the outcome of an update'
    - 'Gathers a change into a single setup point by supplying dependencies from outside'
---

# Object-Oriented PHP and Standards

When a definition changes, how much of the code that uses it is affected? Some changes stay inside the definition, some are absorbed at a single wiring point, some stop the code that uses them, some quietly change its result. The course measures classes, inheritance, interfaces, traits, namespaces, autoloading, dependency management and dependency injection with a single question — where does this change reach?

## Object Model

Classes and objects, inheritance and abstract classes, interfaces, traits, static members, magic methods and type declarations — each as a mechanism that decides what the code using a definition experiences when that definition changes.

### Classes and Objects

Source: https://academia.sh/en/courses/php-object-model/classes-and-objects

What the code that uses a class experiences when a name inside the class changes: a private property, a public property, and a consumer that writes to a public property; the four reaches that measure where a change goes.

The previous course measured PHP's language inside a single file: when the type of a value changes, through which channel an error is heard. This course steps outside the file. In a real program the definitions live in one file, the code that uses them lives in others, and one day a definition changes. The question is this: where does this change reach? The course asks it of every mechanism with the same method and answers with one of four names.

**In place:** the change stayed inside the definition, and the consumers gave the same result without being edited. **At the seam:** the change was absorbed at a single seam, a loader or a setup file. **Breaking:** the change reached the consumers and stopped them; the exit code is not 0. **Implicit:** the change reached the consumers, they ran, but the result changed. Breaking and implicit are different from the channel distinction in PHP Fundamentals; there the question was through which channel a problem is heard, while here the results of two runs are compared. What Systems Programming with C measured was different too: there it was a change in data, here it is a change in a definition. Twelve changes will be measured over the course.

#### Class, Property and Method

A class is the mold of objects. It contains properties and methods. A property is the data the object carries, a method is the work the object can do. `new` builds an object, an instance, from the class, and every instance carries its own property values. Inside a method, the object reaches itself with `$this`. The class below also shows this course's file name rule: the name of a class file is the same as the name of the class, classes are written in `StudlyCaps`, and methods and properties in `camelCase`.

```php
<?php
// Account.php — a class that carries a property, a method and a constructor
class Account
{
    public function __construct(private int $balance = 0) {}

    public function deposit(int $amount): void
    {
        $this->balance += $amount;
    }

    public function balance(): int
    {
        return $this->balance;
    }
}
```

`__construct` is the constructor: it runs on the `new` call. The `private` keyword in front of the parameter makes the parameter a property at the same time; a separate property declaration and an assignment line are not needed. `private` is a visibility: only the class's own methods can reach the property. This is encapsulation. The class opens methods to the outside, not data.

```php
<?php
// account_usage.php — two objects carry two separate states; a private property cannot be reached from outside
require 'Account.php';

$first = new Account(10);
$second = new Account();
$first->deposit(50);
$second->deposit(5);
echo $first->balance(), ' ', $second->balance(), "\n";

try {
    echo $first->balance;
} catch (Error $error) {
    echo get_class($error), "\n";
}
```

```
60 5
Error
```

**OM1.** Two objects were built from the same class and the deposited amounts did not mix: `60` and `5`. The line that reached the private property without a method threw `Error`. The only surface open to the consumer code is the methods; the name of the property is not part of that surface. This observation is the reason the measurement below comes out in place.


#### A Property in the Constructor, readonly and the Three Visibilities

Writing a visibility in front of a constructor parameter (constructor promotion) is a shorthand for the long form: the property declaration and the `$this->balance = $balance` assignment are made automatically. The two forms produce the same object.

```php
<?php
// Equivalent.php — the long form and the short form build the same object
class LongForm
{
    private int $balance;

    public function __construct(int $balance = 0)
    {
        $this->balance = $balance;
    }

    public function balance(): int
    {
        return $this->balance;
    }
}

class ShortForm
{
    public function __construct(private int $balance = 0) {}

    public function balance(): int
    {
        return $this->balance;
    }
}

var_dump((new LongForm(7))->balance() === (new ShortForm(7))->balance());
```

```
bool(true)
```

If a property will not change once it is built, `readonly` is written: the value is assigned once in the constructor and cannot be changed from anywhere afterwards. Reading stays open, writing causes an `Error`.

```php
<?php
// Fixed.php — a readonly property is assigned once
class Fixed
{
    public function __construct(public readonly int $number) {}
}

$fixed = new Fixed(4);
echo $fixed->number, "\n";

try {
    $fixed->number = 5;
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
4
Error
```

There are three levels of visibility. `public` is reachable from anywhere, `protected` from inside the class and its subclasses, `private` only from inside the class where it is defined. In the class below all three are read from the inside, while from the outside only `public` can be read.

```php
<?php
// Visibility.php — three visibilities from inside and from outside
class Base
{
    public int $a = 1;
    protected int $b = 2;
    private int $c = 3;

    public function read(): string
    {
        return $this->a . $this->b . $this->c;
    }
}

class Child extends Base
{
    public function peek(): string
    {
        return $this->a . $this->b;
    }
}

$object = new Child();
echo $object->read(), ' ', $object->peek(), "\n";

$base = new Base();
foreach (['a', 'b', 'c'] as $name) {
    try {
        echo $base->$name, ' ';
    } catch (Error $e) {
        echo get_class($e), ' ';
    }
}
echo "\n";
```

```
123 12
1 Error Error
```

`Child` read the `protected` `b` from its own inside but could not reach the `private` `c`; that is why `peek()` joined only `a` and `b`. Looking at a `Base` object from the outside, only `a` could be read. The visibility of a property decides who the code that depends on it can be made of.

#### The Measurement Core

Where a change reaches is measured with two runs: with the old state of the definition and with the new one. The course leaves this job to the file `reach.php`. The file stays the same in every lesson and is set up once.

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

Reading the file from top to bottom is enough. `write_files()` writes files under a directory. The key is the file name and the value is the content; a file whose value is `null` means "deleted" and is not written, and later lessons will describe changes that remove a file with this marker. If a subdirectory is needed, `mkdir` creates it. `read_files()` returns the files of a directory as name and content pairs; it is not called in this lesson, but it is given in full because the file is the course's shared core.

`run()` enters a directory and runs the file `main.php` there with `php`. Two settings are the foundation of the measurement: `display_errors` is turned off and standard error is discarded. That way a warning or an error message does not get mixed into the output; what is measured is not the text of a message but the exit code. The return value is a pair of the exit code and the output.

`measure()` does the real work and takes four parameters: the name of the observation, the baseline files, the change files and a list of seams. The list may be passed empty; in this lesson's observations it is empty. The function first checks the consumer: if the change array contains `main.php` it throws an exception, because a change that edits the consumer is not measured. Then it writes the baseline to the directory `measure/<name>/before`, lays the change over the baseline and writes the result to the directory `measure/<name>/after`. Both directories are run. If the exit code of the earlier run is not 0, the function again throws an exception: a result that comes from a broken baseline does not measure a change.

The `match (true)` block is the decision order, and the reach is given in this order: first breaking, then implicit, then at the seam, and if none of them, in place. The order matters. If a change both alters the output and stops the consumer, it counts as breaking, because stopping is heard earlier. This order is not explained again in any lesson of the course. The last `printf` prints a one-line summary of each observation: the name, the number of edited files, the exit code after the change, whether the output is the same or changed, and the reach. The number of files does not decide the reach; what decides it is whether the consumer was edited.

#### The Private Property Changes Name

The first observation changes the name of the private property of the `Account` class from `balance` to `total`. The consumer `main.php` calls only the `deposit()` and `balance()` methods. The driver runs the `measure()` call and then prints the output of the two runs separately.

```php
<?php
// classes.php — the name of a private property changes; the consumer calls only methods
require_once 'reach.php';

measure('private-property', [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            private int $balance = 0;
            public function deposit(int $amount): void { $this->balance += $amount; }
            public function balance(): int { return $this->balance; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'account.php';
        $account = new Account();
        $account->deposit(50);
        $account->deposit(25);
        echo $account->balance(), "\n";
        PHP,
], [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            private int $total = 0;
            public function deposit(int $amount): void { $this->total += $amount; }
            public function balance(): int { return $this->total; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/private-property/$stage");
    echo $stage, ': ', $output, "\n";
}
```

```
private-property   files=1  exit=0   output same    -> in-place
before: 75
after: 75
```

**OM2.** One file was edited, the exit code stayed 0, the output stayed the same: `75` before and after. The consumer file was never opened. What kept the change inside the class is the visibility; the only thing the consumer could see was the methods, and the signatures of both stayed the same. The reach of this lesson is **in place**. It should not be confused with the "silent" channel of PHP Fundamentals: there a problem gave no signal, here there is no problem at all.

#### When the Property Is Public

The same change gives a different result when the property is `public`. In the opposing measurement the `balance` property of the `Account` class is public and the consumer reads the property directly. The name again becomes `total`; the consumer `main.php` is again not edited.

```php
<?php
// public.php — the same change, but the property is public and the consumer reads it
require_once 'reach.php';

measure('public-property', [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            public int $balance = 0;
            public function deposit(int $amount): void { $this->balance += $amount; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'account.php';
        $account = new Account();
        $account->deposit(50);
        $account->deposit(25);
        var_dump($account->balance);
        PHP,
], [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            public int $total = 0;
            public function deposit(int $amount): void { $this->total += $amount; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/public-property/$stage");
    echo $stage, ': ', $output, "\n";
}
```

```
public-property    files=1  exit=0   output changed -> implicit
before: int(75)
after: NULL
```

**OM3.** The exit code is still 0, but the output changed: `NULL` instead of `int(75)`. The consumer read a property that no longer exists in the definition and got `NULL`. This lesson does not measure the presence of a signal, it measures the comparison of two runs, and the comparison gives the reach **implicit**. The same code and the same change moved from the reach **in place** to the reach **implicit** only because the visibility was different.

#### The Bounding Measurement

The result above depended on the consumer reading the property. If the consumer writes to the same property, the situation changes. In this measurement the name of the `public` property of the `Account` class changes again, but the consumer assigns the value itself before reading it.

```php
<?php
// writer.php — a consumer that writes to a public property does not stop when the name changes
require_once 'reach.php';

measure('public-write', [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            public int $balance = 0;
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'account.php';
        $account = new Account();
        $account->balance = 75;
        echo $account->balance, "\n";
        PHP,
], [
    'account.php' => <<<'PHP'
        <?php
        class Account
        {
            public int $total = 0;
        }
        PHP,
]);
```

```
public-write       files=1  exit=0   output same    -> in-place
```

The reach came out in place, but this does not mean the language protected the consumer. The name `balance` no longer exists in the definition; when a property that does not exist is written, PHP adds it to the object as a dynamic property, and since PHP 8.2 it reports this with `E_DEPRECATED` unless the class is marked with `#[\AllowDynamicProperties]`. The written value comes back when it is read, so `measure()` finds the two outputs the same. Whether the language gave a signal can be measured separately, in a block with an error handler.

```php
<?php
// dynamic.php — which level does writing to a property that does not exist report
$names = [E_DEPRECATED => 'E_DEPRECATED', E_WARNING => 'E_WARNING', E_NOTICE => 'E_NOTICE'];
set_error_handler(function (int $level) use ($names): bool {
    echo 'reported: ', $names[$level] ?? 'other', "\n";
    return true;
});

class Account
{
    public int $total = 0;
}

$account = new Account();
$account->balance = 75;
echo $account->balance, "\n";
```

```
reported: E_DEPRECATED
75
```

**OM4.** The language gave a signal but did not change the class: writing to a property that does not exist reported `E_DEPRECATED` and the value was still read, `75`. The consumer did not stop, because a write talks to the object, not to the definition. An in-place result is not a measure of how sturdy the definition is, but of which part of the definition the consumer touches: the reading consumer lost the name and fell under **implicit**, while the writing consumer stayed **in place** because it recreated the name itself. The lesson measured the level of this notice only by the name `E_DEPRECATED`; the text of the message was not printed.

#### Summary

- A class carries properties and methods, `new` builds an instance, and every instance keeps its own state; `private` visibility closes a property to the outside.
- Where a change reaches is measured with a before and after run and one of four reaches is chosen; the decision order is breaking, implicit, at the seam, in place.
- When the name of a private property changed, the consumer heard nothing because it called methods: `75` before and after. The reach of this lesson is **in place**.
- The same change on a `public` property, with a reading consumer, moved to **implicit**: `NULL` instead of `int(75)`.
- A consumer that writes to a public property did not stop when the name changed; PHP created a dynamic property and reported `E_DEPRECATED`, and `measure()` again found the reach **in place**.

#### Next Step

When the inside of a class changed, its consumers were not affected, because visibility stood in between. The Inheritance lesson asks about the case where a class extends another: when the inside of the parent class changes, what does the subclass experience?

### Inheritance and Abstract Classes

Source: https://academia.sh/en/courses/php-object-model/inheritance-and-abstract-classes

A change inside a parent class shifting the result of a subclass implicitly, an added abstract method stopping the subclasses, and the same counter built with composition leaving the same change in place.

In Classes and Objects a name inside a class changed and the consumer was unaffected, because visibility stood in between. With inheritance nothing stands in between: the subclass calls the parent's methods and protected properties directly. So the question changes. When the inside of the parent class changes, what does the subclass experience?

#### Building a Subclass

Inheritance is a class taking over the properties and methods of another class. The class that takes over is the subclass, the class taken from is the parent class. In PHP the `extends` keyword declares this. The `protected` visibility opens a member to the class itself and its subclasses and keeps it closed to the outside. If a subclass writes a method under the same name, it overrides the method; the parent's version is still reachable with `parent::`.

```php
<?php
// ItemList.php — a parent class that carries a protected property and three methods
class ItemList
{
    protected array $items = [];

    public function add(string $item): void
    {
        $this->items[] = $item;
    }

    public function addAll(array $items): void
    {
        foreach ($items as $item) {
            $this->add($item);
        }
    }

    public function count(): int
    {
        return count($this->items);
    }
}
```

```php
<?php
// CountingList.php — a subclass that overrides the add() method
class CountingList extends ItemList
{
    public int $counter = 0;

    public function add(string $item): void
    {
        $this->counter++;
        parent::add($item);
    }
}
```

```php
<?php
// list_usage.php — the subclass uses the parent's method; the protected property is closed from outside
require 'ItemList.php';
require 'CountingList.php';

$list = new CountingList();
$list->addAll(['a', 'b', 'c']);
echo $list->counter, ' ', $list->count(), "\n";

try {
    echo count($list->items);
} catch (Error $error) {
    echo get_class($error), "\n";
}
```

```
3 3
Error
```

**OM5.** `addAll()` was not written in the subclass, it was inherited; `add()` was called for each item and `CountingList::add()` raised the counter on every call: `3 3`. The `protected` property was open to the subclass but stayed closed to the outside. What stands out here is that the subclass's counter depends on an internal detail of the parent: `addAll()` calling `add()` one by one.

#### Abstract Classes and final

An abstract class cannot be instantiated and can carry methods without a body; subclasses must write those methods. The class below declares one method without a body and supplies one method with a body.

```php
<?php
// Report.php — a method without a body and a method with a body that depends on it
abstract class Report
{
    abstract public function title(): string;

    public function render(): string
    {
        return strtoupper($this->title());
    }
}
```

```php
<?php
// Daily.php — a concrete subclass that writes the abstract method
class Daily extends Report
{
    public function title(): string
    {
        return 'daily';
    }
}
```

```php
<?php
// report_usage.php — the concrete subclass is built, the abstract class cannot be
require 'Report.php';
require 'Daily.php';

echo (new Daily())->render(), "\n";

try {
    new Report();
} catch (Error $error) {
    echo get_class($error), "\n";
}
```

```
DAILY
Error
```

If a method is marked `final`, subclasses cannot override it. The file stops as soon as it is loaded. The script below writes two classes, one of which overrides a `final` method, into a file and prints the exit code.

```bash
# final.sh — a class that overrides a final method cannot be loaded
cat > Fixed.php <<'PHP'
<?php
class Base
{
    final public function identity(): string { return 'base'; }
}
class Child extends Base
{
    public function identity(): string { return 'child'; }
}
PHP
php -d display_errors=0 Fixed.php; echo "exit=$?"
```

```
exit=255
```

**OM6.** `new Daily()` ran and `Report::render()` used the subclass's `title()` method. `new Report()` and the file that overrides a `final` method, however, gave a signal: the first threw `Error`, the second stopped with exit 255. The abstract class and `final` are the two ends of inheritance: one imposes an obligation to write on the subclass, the other takes away the authority to write.

#### The Parent's Constructor

If a subclass writes its own constructor, the parent's constructor does not run on its own. For the state the parent builds to be ready, `parent::__construct()` is called explicitly. If the call is forgotten, the parent's properties are left unset.

```php
<?php
// Person.php — a parent constructor, a subclass that calls it and one that forgets
class Person
{
    public function __construct(protected string $name) {}

    public function greet(): string
    {
        return 'hello ' . $this->name;
    }
}

class Employee extends Person
{
    public function __construct(string $name, private int $number)
    {
        parent::__construct($name);
    }

    public function card(): string
    {
        return $this->greet() . ' #' . $this->number;
    }
}

class Forgetful extends Person
{
    public function __construct(private int $number) {}

    public function read(): string
    {
        return $this->name;
    }
}

echo (new Employee('ali', 7))->card(), "\n";

try {
    echo (new Forgetful(1))->read();
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
hello ali #7
Error
```

Because `Employee` called the parent's constructor, `name` was set. Because `Forgetful` did not, `name` was never written and reading it raised `Error`. In inheritance the constructor chain works only if it is written.

#### Signatures When Overriding and a final Class

When a subclass overrides a method, it has to stay compatible with the parent's signature; code that holds the parent in a typed variable must be able to make the same call with the subclass. Adding a required parameter breaks that compatibility. If a class is not to be extended at all, it is marked `final`; the file that extends it stops as soon as it is loaded.

```bash
# signature.sh — adding a required parameter and extending a final class make the declaration impossible
cat > Signature.php <<'PHP'
<?php
class Base { public function add(string $item): void {} }
class Child extends Base { public function add(string $item, int $quantity): void {} }
PHP
cat > ClosedClass.php <<'PHP'
<?php
final class Closed {}
class Opener extends Closed {}
PHP
php -d display_errors=0 Signature.php; echo "signature exit=$?"
php -d display_errors=0 ClosedClass.php; echo "final exit=$?"
```

```
signature exit=255
final exit=255
```

In both cases the error came not during execution but during the class declaration. A `final` method takes one behavior away from subclasses, a `final` class takes all of it.

#### When Inheritance Fits

Inheritance fits when there is an "is a" relationship between two classes: an `Employee` is a `Person`, a `Daily` is a `Report`. Inheritance built only to share code ties the subclass to all of the parent's methods and to every future change in them. This is exactly what is measured throughout this lesson: when the inside of the parent changes, the subclasses tied to it are affected. If the shared code describes a capability rather than a relationship, an interface or a trait creates fewer ties; they are the next two lessons.

#### The Measurement Core

The measurement in this lesson uses the file `reach.php`; this is the core explained line by line in Classes and Objects, and it is rewritten in every lesson's working directory.

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

#### The Parent's Inside Changes

The first observation uses the `ItemList` and `CountingList` pair above. The parent's `addAll()` method no longer calls `add()` one by one; it merges the array in one go with `array_merge()`. The subclass and the consumer `main.php` stay the same. The contract the parent offers to the outside does not change either: the two methods carry the same name and signature.

```php
<?php
// inheritance.php — the parent no longer calls the single-add method in bulk add
require_once 'reach.php';

measure('parent-class', [
    'item_list.php' => <<<'PHP'
        <?php
        class ItemList
        {
            protected array $items = [];
            public function add(string $item): void { $this->items[] = $item; }
            public function addAll(array $items): void
            {
                foreach ($items as $item) {
                    $this->add($item);
                }
            }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'item_list.php';
        class CountingList extends ItemList
        {
            public int $counter = 0;
            public function add(string $item): void { $this->counter++; parent::add($item); }
        }
        $list = new CountingList();
        $list->addAll(['a', 'b', 'c']);
        echo $list->counter, "\n";
        PHP,
], [
    'item_list.php' => <<<'PHP'
        <?php
        class ItemList
        {
            protected array $items = [];
            public function add(string $item): void { $this->items[] = $item; }
            public function addAll(array $items): void
            {
                $this->items = array_merge($this->items, $items);
            }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/parent-class/$stage");
    echo $stage, ': ', $output, "\n";
}
```

```
parent-class       files=1  exit=0   output changed -> implicit
before: 3
after: 0
```

**OM7.** The exit code is 0, but the output changed: the counter went from `3` to `0`. The subclass had overridden only the `add()` method; since the parent no longer calls `add()`, the subclass's method never ran. There is no error and no warning in the consumer, only a wrong number. This is the fragile base class problem: the subclass trusted not the parent's contract but its inner workings, and the inner workings changed. The lesson's reach is **implicit**. In Object-Oriented Java what was measured was which type chooses the method that gets called; here what is measured is whether a change in the parent reaches the subclass, and it did.

#### Adding an Abstract Method

The opposing measurement moves the change from the inside of the parent to its contract. A new `abstract` method is added to an abstract class. A subclass written earlier, like `Daily`, does not know this method.

```php
<?php
// abstract.php — a new abstract method is added to the abstract parent
require_once 'reach.php';

measure('abstract-method', [
    'report.php' => <<<'PHP'
        <?php
        abstract class Report
        {
            abstract public function title(): string;
            public function render(): string { return strtoupper($this->title()); }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'report.php';
        class Daily extends Report
        {
            public function title(): string { return 'daily'; }
        }
        echo (new Daily())->render(), "\n";
        PHP,
], [
    'report.php' => <<<'PHP'
        <?php
        abstract class Report
        {
            abstract public function title(): string;
            abstract public function footer(): string;
            public function render(): string { return strtoupper($this->title()); }
        }
        PHP,
]);
```

```
abstract-method    files=1  exit=255 output changed -> breaking
```

**OM8.** The same consumer, the same subclass, the same single-file change; this time the consumer stopped. An abstract method tells a subclass "you must write this", and when the subclass does not write it, the class cannot be declared. The exit code is not 0, the reach is **breaking**. Because the contract has no body, the language catches the change; there was no contract that would catch an internal change in the parent.

#### The Bounding Measurement

Is the cause of the implicit result inheritance itself, or the counter being tied to the inside of the parent? When the same counter is built with composition, the measurement separates the two. In composition the class does not extend another class, it carries an instance of it and counts in its own two methods. The change made in the parent is the same as before: `addAll()` no longer calls `add()`.

```php
<?php
// composition.php — the counter is built with composition, not inheritance; the same parent change
require_once 'reach.php';

measure('composition', [
    'item_list.php' => <<<'PHP'
        <?php
        class ItemList
        {
            protected array $items = [];
            public function add(string $item): void { $this->items[] = $item; }
            public function addAll(array $items): void
            {
                foreach ($items as $item) {
                    $this->add($item);
                }
            }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'item_list.php';
        class CountingList
        {
            public int $counter = 0;
            private ItemList $inner;
            public function __construct() { $this->inner = new ItemList(); }
            public function add(string $item): void { $this->counter++; $this->inner->add($item); }
            public function addAll(array $items): void { $this->counter += count($items); $this->inner->addAll($items); }
        }
        $list = new CountingList();
        $list->addAll(['a', 'b', 'c']);
        echo $list->counter, "\n";
        PHP,
], [
    'item_list.php' => <<<'PHP'
        <?php
        class ItemList
        {
            protected array $items = [];
            public function add(string $item): void { $this->items[] = $item; }
            public function addAll(array $items): void
            {
                $this->items = array_merge($this->items, $items);
            }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/composition/$stage");
    echo $stage, ': ', $output, "\n";
}
```

```
composition        files=1  exit=0   output same    -> in-place
before: 3
after: 3
```

**OM9.** The parent's change is the same, the consumer is the same; the result stayed `3`. In composition the counter does not look at which method of the parent calls which: it counts in its own two methods and reaches the parent only through the contract, that is, the names and signatures of the two methods. The same internal change was implicit in inheritance and stayed in place in composition. The distinction measured is one of design, not of language: inheritance ties the subclass to the inside of the parent, composition does not. The cost comes from the same place: `CountingList` is no longer an `ItemList`.

#### Summary

- `extends` builds a subclass, `protected` opens a member to the subclass while keeping it closed to the outside, and `parent::` reaches the parent's method.
- An abstract class cannot be instantiated and obliges subclasses to write the methods without a body; a `final` method cannot be overridden.
- When the parent stopped calling `add()` in bulk add, the subclass's counter dropped from `3` to `0`; the reach of this lesson is **implicit**.
- Adding an `abstract` method to the abstract class stopped the same consumer, a **breaking** reach; because the contract has no body, the language catches it.
- When the counter was built with composition, the same parent change stayed in place: `3`.

#### Next Step

The abstract method brought a contract without a body, and adding a method to the contract stopped the subclass. The Interfaces lesson separates this contract from inheritance: when a method is added to an interface, what do the classes that implement it experience?

### Interfaces

Source: https://academia.sh/en/courses/php-object-model/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.

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
<?php
// Payable.php — an interface that carries only a signature
interface Payable
{
    public function amount(): int;
}
```

```php
<?php
// Printable.php — a second interface
interface Printable
{
    public function text(): string;
}
```

```php
<?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
<?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
<?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
<?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
<?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
<?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
<?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.

```bash
# 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
<?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
<?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
<?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
<?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.

```bash
# 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; `instanceof` says which interface is implemented.
- An interface checks the signature, not the meaning.
- When the `currency()` signature was added to the interface, `Invoice` could not be declared, with exit 255 instead of `120`; 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?

### Traits

Source: https://academia.sh/en/courses/php-object-model/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.

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
<?php
// Logged.php — a trait that carries the record-keeping behavior
trait Logged
{
    public function summary(): string
    {
        return 'record';
    }
}
```

```php
<?php
// Timed.php — a second trait that carries time information
trait Timed
{
    public function time(): string
    {
        return '12:00';
    }
}
```

```php
<?php
// Document.php — a class that uses two traits together
require_once 'Logged.php';
require_once 'Timed.php';

class Document
{
    use Logged, Timed;
}
```

```php
<?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
<?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
<?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
<?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
<?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.

```bash
# 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
<?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
<?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
<?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
<?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
<?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
<?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 `use`s 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
<?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?

### Static Members and Constants

Source: https://academia.sh/en/courses/php-object-model/static-members-and-constants

Properties, methods, and constants bound to the class, hiding the constructor behind a static factory method, and which changes stop the consumer.

Traits shared methods; this lesson is about members bound not to an object but to the class itself. The question is this: once object creation moves into a static factory method, why does adding a parameter to the constructor not stop the consumer, and why would it have been different if the constructor had stayed open?

#### Static Members

A static member belongs to the class and is reached with `ClassName::` without creating an object. From inside the class, the same class is reached with `self::`. A static property has a single copy per class, and all objects share it. A class constant is declared with `const`, its value never changes, and it is read with `self::RATE` or `Tax::RATE`.

```php
<?php
// Tax.php — a constant, property, and method bound to the class
class Tax
{
    public const RATE = 20;
    public static int $computed = 0;

    public static function add(int $amount): int
    {
        self::$computed++;
        return $amount + intdiv($amount * self::RATE, 100);
    }
}
```

```php
<?php
// tax_usage.php — static members are reached without an object
require 'Tax.php';

echo Tax::add(100), "\n";
echo Tax::add(200), "\n";
echo Tax::RATE, ' ', Tax::$computed, "\n";
```

```
120
240
20 2
```

**OM17.** No object was created for `Tax`, and `add()` ran twice; `$computed` counted both calls in a single copy. This sharing was measured in M11/K03 with the question of who sees what, and it is not measured again here. The use of `static::`, which asks for the class to be resolved again in a subclass (late static binding), is the subject of M08/K02 and is not measured here.

#### Static Factory Method

A static factory method is a static method that creates an object. When the constructor is made `private`, the object can come only from this method. The class thus limits for itself which values an object can be created with.

```php
<?php
// Money.php — the constructor is hidden, a static factory creates the object
class Money
{
    private function __construct(private int $cents) {}

    public static function dollars(int $dollars): self
    {
        return new self($dollars * 100);
    }

    public function cents(): int
    {
        return $this->cents;
    }
}
```

```php
<?php
// money_usage.php — the object is created only through the factory
require 'Money.php';

echo Money::dollars(5)->cents(), "\n";

try {
    $money = new Money(500);
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
500
Error
```

**OM18.** The factory turned `5` dollars into `500` cents, while a direct `new Money(500)` raised an `Error` because the constructor is closed. The consumer's only path became `Money::dollars()`. The question of this lesson is why that single path can be changed.

#### Object Context and Static Context

An instance method works on an object, and `$this` is that object. A static method has no object, so `$this` cannot be used; only static members are reached, with `self::`. The same class can carry both contexts: an instance property is separate in each object, while a static property is a single copy.

```php
<?php
// Counter.php — an instance property and a static property together
class Counter
{
    private static int $total = 0;
    private int $local = 0;

    public function increment(): string
    {
        self::$total++;
        $this->local++;
        return $this->local . '/' . self::$total;
    }

    public static function total(): int
    {
        return self::$total;
    }

    public static function broken(): int
    {
        return $this->local;
    }
}
```

```php
<?php
// counter_usage.php — the local counters of two objects are separate, the total is shared
require 'Counter.php';

$first = new Counter();
$second = new Counter();

echo $first->increment(), ' ', $first->increment(), ' ', $second->increment(), "\n";
echo Counter::total(), "\n";

try {
    Counter::broken();
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
1/1 2/2 1/3
3
Error
```

`$second` saw `1` on its own counter but `3` in the total: both calls of the first object had written to the same static property. Looking for `$this` in a static method raised an `Error`. A static call that resolves differently in subclasses uses `static::`; this behavior, called late static binding, was measured in M08/K02 and is not repeated here.

#### Visibility of a Class Constant

A class constant carries visibility just like a property and a method. A `public const` is read from anywhere, a `private const` only from inside the class itself. A constant definition may rest on another constant, but not on a non-constant expression such as a function call.

```php
<?php
// Settings.php — constants with different visibility
class Settings
{
    public const NAME = 'panel';
    private const ORDER = 5;
    public const FACTOR = self::ORDER * 2;

    public static function order(): int
    {
        return self::ORDER;
    }
}
```

```php
<?php
// settings_usage.php — a private constant cannot be reached from outside the class
require 'Settings.php';

echo Settings::NAME, ' ', Settings::FACTOR, ' ', Settings::order(), "\n";

try {
    echo Settings::ORDER;
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
panel 10 5
Error
```

`FACTOR` was derived from `ORDER` inside the class and could be read from outside; `ORDER` left the class only through the `order()` method. This shows that the property visibility from the Classes and Objects lesson applies to a constant unchanged: making a constant `private` limits which consumers a change in its value can reach.

#### The Measurement Core

The measurement in this lesson also uses the `reach.php` file; the core was explained line by line in the first lesson.

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

#### Adding a Parameter to the Factory

The measured change is adding a second parameter named `currency` to the constructor of the `Money` class. The factory supplies this parameter itself: `dollars()` passes `'USD'` to the constructor. The consumer `main.php` prints the value of `Money::dollars(5)->cents()`.

```php
<?php
// factory.php — a parameter is added to the constructor, the factory gives the consumer the same value unchanged
require_once 'reach.php';

measure('static-factory', [
    'money.php' => <<<'PHP'
        <?php
        class Money
        {
            private function __construct(private int $cents) {}
            public static function dollars(int $dollars): self { return new self($dollars * 100); }
            public function cents(): int { return $this->cents; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'money.php';
        echo Money::dollars(5)->cents(), "\n";
        PHP,
], [
    'money.php' => <<<'PHP'
        <?php
        class Money
        {
            private function __construct(private int $cents, private string $currency) {}
            public static function dollars(int $dollars): self { return new self($dollars * 100, 'USD'); }
            public function cents(): int { return $this->cents; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/static-factory/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
static-factory     files=1  exit=0   output same    -> in-place
before exit=0 output=500
after exit=0 output=500
```

**OM19.** The signature of the constructor changed, but because what the consumer sees is `Money::dollars(5)->cents()`, the output stayed `500`. Only the factory called the constructor, and the party that wrote the factory supplied the new parameter inside it. The lesson's first reach is **in place**.

#### The Bounding Measurement

Is the change staying in place because of the factory, or because of the change itself? We can separate the two by leaving the constructor open and letting the consumer say `new Money(500)`: when the same parameter is added, the consumer depends directly on the constructor.

```php
<?php
// public_constructor.php — the constructor is open, the consumer calls it directly with new
require_once 'reach.php';

measure('public-constructor', [
    'money.php' => <<<'PHP'
        <?php
        class Money
        {
            public function __construct(private int $cents) {}
            public function cents(): int { return $this->cents; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'money.php';
        echo (new Money(500))->cents(), "\n";
        PHP,
], [
    'money.php' => <<<'PHP'
        <?php
        class Money
        {
            public function __construct(private int $cents, private string $currency) {}
            public function cents(): int { return $this->cents; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/public-constructor/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
public-constructor files=1  exit=255 output changed -> breaking
before exit=0 output=500
after exit=255 output=
```

**OM20.** The same added parameter, the same class; but once the consumer called the constructor itself, the second parameter became required and the exit code was 255. So what kept the change in place was not the parameter itself but the fact that the consumer could not touch the constructor at all. The factory gathers the dependency on the constructor into a single point.

A class constant belongs to another group. If the value of a class constant changes, nothing stops the consumer, because the type and the signature stay the same.

```php
<?php
// constant.php — the value of a class constant changes, the signature stays the same
require_once 'reach.php';

measure('class-constant', [
    'tax.php' => <<<'PHP'
        <?php
        class Tax
        {
            public const RATE = 20;
            public static function add(int $amount): int { return $amount + intdiv($amount * self::RATE, 100); }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'tax.php';
        echo Tax::add(100), "\n";
        PHP,
], [
    'tax.php' => <<<'PHP'
        <?php
        class Tax
        {
            public const RATE = 18;
            public static function add(int $amount): int { return $amount + intdiv($amount * self::RATE, 100); }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/class-constant/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
class-constant     files=1  exit=0   output changed -> implicit
before exit=0 output=120
after exit=0 output=118
```

**OM21.** `RATE` dropped from `20` to `18`, the consumer received no error, but the value it received moved from `120` to `118`: exit code 0, output changed. Because the value of a constant is not treated as a contract, nothing stopped. This reach is **implicit**.

#### A Static Property in a Subclass

A subclass inherits the static property of its parent but does not get a new copy: the two share the same storage. If the subclass redeclares the property in its own body, a separate copy is born. The difference between the two matters when structures such as a counter or a registry are extended through inheritance.

```php
<?php
// Sharing.php — the static property of the parent is shared with the subclass
class Base
{
    public static int $count = 0;
}

class Shared extends Base
{
}

class Separate extends Base
{
    public static int $count = 0;
}
```

```php
<?php
// sharing_usage.php — Shared shares with Base, Separate does not
require 'Sharing.php';

Shared::$count = 5;
echo Base::$count, "\n";

Separate::$count = 9;
echo Base::$count, ' ', Separate::$count, "\n";
```

```
5
5 9
```

The expression `Shared::$count` actually wrote to the property of `Base`; that is why `Base::$count` also gave `5`. `Separate` redeclared the property, so it kept its own value and did not affect `Base`. Code that wants to give subclasses separate counters has to redeclare the property in every subclass; if it is forgotten, the counters merge silently.

#### Static Members and Difficulty in Testing

Because a static member is shared and reachable from anywhere by the class name, what state a piece of code runs in depends on the earlier calls. Test scenarios should be independent of one another; static state breaks this independence. The two scenarios below are written separately, but the second sees the data the first left behind.

```php
<?php
// Cache.php — shared state reached by the class name
class Cache
{
    private static array $data = [];

    public static function put(string $key, int $value): void
    {
        self::$data[$key] = $value;
    }

    public static function size(): int
    {
        return count(self::$data);
    }
}
```

```php
<?php
// scenario.php — the second scenario sees the data the first one left behind
require 'Cache.php';

function firstScenario(): int
{
    Cache::put('a', 1);
    return Cache::size();
}

function secondScenario(): int
{
    return Cache::size();
}

echo firstScenario(), ' ', secondScenario(), "\n";
```

```
1 1
```

`secondScenario()` put nothing in and still saw `1`. Run on its own it would have seen `0`; run in sequence, the order of execution determines the result. Keeping the same data as a property of an object and passing the object in from outside breaks this dependency: each scenario creates its own object and does not see what another left behind. Passing a dependency in from outside will be covered separately at the end of the course; this lesson only showed why static state calls for that solution.

#### Summary

- A static member is bound to the class; it is reached with `self::`, a static property has a single copy per class, and a class constant never changes.
- Hiding the constructor and creating the object through a static factory method gathers the way objects are created into a single point.
- When a parameter was added to the constructor, the consumer that creates through the factory stayed in place, with output `500`; the lesson's reach is in place.
- The same addition was breaking when the constructor was open and the consumer said `new Money(500)`, with exit code 255.
- When the value of a class constant dropped from `20` to `18`, the consumer was affected implicitly: exit code 0, output moved from `120` to `118`.

#### Next Step

A constant's value shifted the consumer silently; magic methods, on the other hand, turn reading itself into a method. The Magic Methods lesson asks what the consumer sees when the content of the `__get` method, through which a property is read, changes.

### Magic Methods

Source: https://academia.sh/en/courses/php-object-model/magic-methods

The __get, __set, __isset, __call, and __toString methods, resolving a name that is read through a method, and the implicit and breaking results the consumer meets when that resolution changes.

In the Static Members and Constants lesson, the consumer shifted without an error when the value of a constant changed. With magic methods the same silence spreads over a larger area, because reading a property or calling a method can now be turned into a method inside the class. The question is what the consumer sees when the internal name behind this translation changes.

#### Magic Methods

Magic methods are methods whose names begin with two underscores and which PHP itself calls in certain situations. `__get` runs when a property that does not exist or is inaccessible is read, `__set` when it is written, `__isset` when it is queried with `isset()`, `__call` when a method that does not exist is called, and `__toString` when an object is used as a string.

```php
<?php
// Setting.php — a class that catches reading, writing, querying, method calls, and conversion to a string
class Setting
{
    private array $values = [];

    public function __set(string $name, mixed $value): void
    {
        $this->values[$name] = $value;
    }

    public function __get(string $name): mixed
    {
        return $this->values[$name] ?? null;
    }

    public function __isset(string $name): bool
    {
        return isset($this->values[$name]);
    }

    public function __call(string $name, array $args): string
    {
        return $name . '(' . count($args) . ')';
    }

    public function __toString(): string
    {
        return 'Setting[' . count($this->values) . ']';
    }
}
```

```php
<?php
// setting_usage.php — magic methods are triggered by the expressions the consumer writes
require 'Setting.php';

$setting = new Setting();
$setting->theme = 'dark';
echo $setting->theme, "\n";
var_dump(isset($setting->theme), isset($setting->lang));
echo $setting->save(1, 2), "\n";
echo $setting, "\n";
```

```
dark
bool(true)
bool(false)
save(2)
Setting[1]
```

**OM22.** No property named `theme` was defined, and the consumer could still write and read it: `__set` put the value into the `$values` array and `__get` took it back out. There was no `save()` method either; `__call` returned the name and the number of arguments. This differs from the dynamic property in the Classes and Objects lesson: there the property was created on the object and raised a deprecation notice (`E_DEPRECATED`), while here a defined method answers.

#### __unset and __callStatic

`__unset` runs when an inaccessible property is removed with `unset()`; it is written together with `__isset`, because both look at the same data structure. `__callStatic` is the static counterpart of `__call`: it takes over when a method that is not in the class is called in the form `ClassName::name()`. Both pass the name and the arguments to the body of the method.

```php
<?php
// Record.php — a class that catches removal and static calls
class Record
{
    private array $data = ['a' => 1, 'b' => 2];

    public function __isset(string $name): bool
    {
        return isset($this->data[$name]);
    }

    public function __unset(string $name): void
    {
        unset($this->data[$name]);
    }

    public static function __callStatic(string $name, array $args): string
    {
        return 'static ' . $name . '(' . count($args) . ')';
    }
}
```

```php
<?php
// record_usage.php — unset and a static call go to magic methods
require 'Record.php';

$record = new Record();
var_dump(isset($record->a));
unset($record->a);
var_dump(isset($record->a));
echo Record::find(7), "\n";
```

```
bool(true)
bool(false)
static find(1)
```

Because `$data` is private, `$record->a` was inaccessible from outside; `isset` and `unset` therefore fell to the magic methods and removed the key in the array. Had `__unset` not been written, `unset($record->a)` would have passed without doing anything.

#### A Magic Method Runs Only for an Inaccessible Name

A magic read method is not a shortcut: PHP first looks for the name among the real properties of the class and, if it finds it, never calls the method. `__get` runs only if the name does not exist or is not visible from the calling place. This distinction determines how moving a `public` property to a magic method later affects the consumer.

```php
<?php
// Vault.php — __get does not run while a real property exists
class Vault
{
    public string $open = 'o';
    private string $hidden = 'h';

    public function __get(string $name): string
    {
        return 'magic:' . $name;
    }
}
```

```php
<?php
// vault_usage.php — __get runs only for an inaccessible name
require 'Vault.php';

$vault = new Vault();
echo $vault->open, ' ', $vault->hidden, ' ', $vault->missing, "\n";

unset($vault->open);
echo $vault->open, "\n";
```

```
o magic:hidden magic:missing
magic:open
```

`open` was real and visible, so it did not reach the magic method; `hidden` did because it is not visible from outside, and `missing` because it never existed. After `open` was removed, its name could no longer be found as a real property and `__get` took over. This behavior shows that a magic method can silently take the place of a real property.

#### __toString and Stringable

`__toString` runs when an object enters a string context: `echo`, concatenation, a function parameter that expects a string. In PHP 8, every class that defines this method is counted as having implemented the `Stringable` interface on its own; the interface declaration is not needed. If an object without the method enters a string context, an `Error` is raised.

```php
<?php
// Tag.php — __toString and Stringable
class Tag
{
    public function __construct(private string $name)
    {
    }

    public function __toString(): string
    {
        return '<' . $this->name . '>';
    }
}

class Plain
{
}

$tag = new Tag('b');
echo $tag, ' ', strlen($tag), ' ', $tag instanceof Stringable ? 'yes' : 'no', "\n";
echo 'Text: ' . $tag . "\n";

try {
    echo new Plain();
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
<b> 3 yes
Text: <b>
Error
```

`strlen()` converted the object to a string and counted `3` on `<b>`. Although the `Stringable` declaration was written nowhere, `instanceof` came out true. Adding `__toString` to a class lets that class's objects into every place that expects a string; this convenience means that the class alone decides the form of the conversion, with no say for the consumer.

#### __set and Typos

When `__set` accepts every unknown name, code that misspells a name gets no error. The misspelled name becomes a new key in the array, while the correct name stays at its old value. If `?? null` is left on the read side, reading a wrong name also silently gives `null`.

```php
<?php
// Flexible.php — __set and __get that accept every name
class Flexible
{
    public string $title = '';
    private array $extra = [];

    public function __set(string $name, mixed $value): void
    {
        $this->extra[$name] = $value;
    }

    public function __get(string $name): mixed
    {
        return $this->extra[$name] ?? null;
    }
}

$flexible = new Flexible();
$flexible->title = 'first';
$flexible->titl = 'second';
echo $flexible->title, ' ', $flexible->titl, "\n";
var_dump($flexible->titlee);
```

```
first second
NULL
```

`titl` was a typo and `__set` accepted it without question; `title` stayed at its first value. No error came when `titlee` was read either. If the party that writes the magic method does not limit the accepted names to a list, the consumer loses spelling checks.

#### Forwarding with __call

`__call` serves a common purpose: forwarding to another object. The wrapping class passes every method it does not know on to the object inside. The consumer calls the wrapper, but the method of the inner object actually runs; a name that the inner object does not have either again raises an `Error`.

```php
<?php
// Wrapper.php — a class that forwards an unknown method to the inner object
class Wrapper
{
    public function __construct(private ArrayObject $inner)
    {
    }

    public function __call(string $name, array $args): mixed
    {
        return $this->inner->$name(...$args);
    }
}

$wrapper = new Wrapper(new ArrayObject([3, 1, 2]));
echo $wrapper->count(), "\n";

try {
    $wrapper->absent();
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
3
Error
```

`count()` was not defined on the wrapper and was answered by `ArrayObject` through the forwarding. This forwarding hides the methods of the wrapped class from the declaration of the wrapper; which names work is learned not by reading the code but by running it.

#### __invoke

`__invoke` allows an object to be called like a function. The object is called in the form `$object(argument)` and PHP runs the body of the method. For this reason the object is accepted as `callable`: it can be given as a callback to a function such as `array_map`. When a transformation that carries state is needed, this feature takes the properties of the object along with the call.

```php
<?php
// Multiplier.php — calling an object like a function
class Multiplier
{
    public function __construct(private int $factor)
    {
    }

    public function __invoke(int $number): int
    {
        return $number * $this->factor;
    }
}

$three = new Multiplier(3);
echo $three(4), ' ', implode(',', array_map($three, [1, 2, 3])), ' ', var_export(is_callable($three), true), "\n";
```

```
12 3,6,9 true
```

`$three(4)` became `__invoke(4)` directly; `array_map` called the object for each element of the array. Because the factor stays in the object, reusing the transformation with a different factor came down to creating a new object.

#### The Measurement Core

The measurement in this lesson also uses the `reach.php` file; the core was explained line by line in the first lesson.

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

#### The Internal Name Changes

The measured change is renaming the `theme` key in the internal array of the `Setting` class to `display`. `__get` and no other part of the class changes. The consumer `main.php` prints the value of `(new Setting())->theme` with `var_dump`.

```php
<?php
// magic_read.php — the internal key that __get looks at is renamed
require_once 'reach.php';

measure('magic-read', [
    'setting.php' => <<<'PHP'
        <?php
        class Setting
        {
            private array $values = ['theme' => 'dark'];
            public function __get(string $name): mixed { return $this->values[$name] ?? null; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'setting.php';
        var_dump((new Setting())->theme);
        PHP,
], [
    'setting.php' => <<<'PHP'
        <?php
        class Setting
        {
            private array $values = ['display' => 'dark'];
            public function __get(string $name): mixed { return $this->values[$name] ?? null; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/magic-read/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
magic-read         files=1  exit=0   output changed -> implicit
before exit=0 output=string(4) "dark"
after exit=0 output=NULL
```

**OM23.** The consumer saw nothing: the exit code stayed 0, but `NULL` came in place of `string(4) "dark"`. `__get` answered an unknown name with `null`, so `theme` no longer existed and nothing said so. The lesson's first reach is **implicit**. Because a magic method turns reading from a contract into a lookup, the rename breaks no signature.

#### The Bounding Measurement

Is the silence caused by the rename, or by `__get` answering an unknown name with `null`? The second option is measured: the same rename is made, but this time `__get` throws an `OutOfRangeException` for an unknown name.

```php
<?php
// unknown_name.php — the same rename, this time __get rejects the unknown name
require_once 'reach.php';

measure('unknown-name', [
    'setting.php' => <<<'PHP'
        <?php
        class Setting
        {
            private array $values = ['theme' => 'dark'];
            public function __get(string $name): mixed
            {
                if (!array_key_exists($name, $this->values)) {
                    throw new OutOfRangeException($name);
                }
                return $this->values[$name];
            }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'setting.php';
        var_dump((new Setting())->theme);
        PHP,
], [
    'setting.php' => <<<'PHP'
        <?php
        class Setting
        {
            private array $values = ['display' => 'dark'];
            public function __get(string $name): mixed
            {
                if (!array_key_exists($name, $this->values)) {
                    throw new OutOfRangeException($name);
                }
                return $this->values[$name];
            }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/unknown-name/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
unknown-name       files=1  exit=255 output changed -> breaking
before exit=0 output=string(4) "dark"
after exit=255 output=
```

**OM24.** The same rename, the same consumer; but the exit code became 255, because the name `theme` was no longer known and `__get` reported this with an exception. What produced the silence was not the rename but `__get` brushing the unknown name aside. The answer of a magic method is the decision of the class author: the same change can be implicit with `null` and breaking with an exception.

#### Summary

- Magic methods such as `__get`, `__set`, `__isset`, `__call`, and `__toString` are methods whose names begin with two underscores and which PHP itself calls.
- Once reading, writing, and method calls are turned into magic methods, an undefined name does not stop the consumer.
- When the internal key that `__get` looks at went from `theme` to `display`, the consumer got no error, with exit code 0; `NULL` came in place of `string(4) "dark"`, and the lesson's reach is implicit.
- When `__get` threw an `OutOfRangeException` for an unknown name, the same change became breaking, with exit code 255.
- Whether the same change is implicit or breaking is decided by how the magic method receives an unknown name.

#### Next Step

Magic methods put a loose translator in place; type declarations do the opposite and place a parameter, a return, and a property type at the boundary of the class. The Type Declarations lesson asks which inputs stop the consumer when `int` is written on the parameter of a method.

### Type Declarations

Source: https://academia.sh/en/courses/php-object-model/type-declarations

Parameter, return, and property types, ?int and the union type, and which inputs stop the consumer when a type is added to a parameter.

Magic methods resolved names loosely; a type declaration places a gate at the boundary of the class. The question is which inputs stop the consumer when `int` is written on an untyped parameter.

#### Type Declarations

A type written before the name of a parameter, a return value, or a property limits the values PHP will accept at that point. `?int` means either `int` or `null`. `int|string` is a union type: it accepts one of two types.

```php
<?php
// Vat.php — parameter, return, ?int, and union type
class Vat
{
    public static function add(int $amount): float
    {
        return $amount * 1.2;
    }

    public static function describe(?int $count): string
    {
        return $count === null ? 'none' : 'some';
    }

    public static function kind(int|string $code): string
    {
        return gettype($code);
    }
}
```

```php
<?php
// vat_usage.php — inputs that match the type pass, those that do not raise a TypeError
require 'Vat.php';

var_dump(Vat::add(100));
echo Vat::describe(null), ' ', Vat::describe(7), "\n";
echo Vat::kind(5), ' ', Vat::kind('a'), "\n";

try {
    Vat::add('abc');
} catch (TypeError $e) {
    echo get_class($e), "\n";
}
```

```
float(120)
none some
integer string
TypeError
```

**OM25.** `add()` took only `int` and threw a `TypeError` when `'abc'` came; `describe()` accepted `null` because its type was `?int`. The details of this behavior, that is `TypeError`, `E_DEPRECATED`, and `strict_types`, were measured in the Variables and Data Types lesson of M12/K01 and are not repeated here. This lesson asks whether the addition of a type declaration reaches the consumer.

#### Return Types: void and never

`void` says that the method returns no value; the result of the call is always `null`, and binding it to a value is meaningless. `never` is a stronger promise: the method never returns; it either throws an exception, ends the process, or runs forever. If a method declared `never` quietly comes to an end, PHP reports this with a `TypeError`.

```php
<?php
// Flow.php — the void and never return types
class Flow
{
    public static function write(string $message): void
    {
        echo $message, "\n";
    }

    public static function halt(string $reason): never
    {
        throw new RuntimeException($reason);
    }

    public static function brokenHalt(): never
    {
        echo "inner\n";
    }
}

var_dump(Flow::write('a'));

try {
    Flow::halt('x');
} catch (RuntimeException $e) {
    echo get_class($e), ' ', $e->getMessage(), "\n";
}

try {
    Flow::brokenHalt();
} catch (TypeError $e) {
    echo get_class($e), "\n";
}
```

```
a
NULL
RuntimeException x
inner
TypeError
```

`write()` wrote something and the value of the call came out `null`. `halt()` never returned, and the exception was caught. `brokenHalt()` ran its body but received a `TypeError` when it tried to return at the end; code that does not keep the promise of `never` is caught by the type itself.

#### Property Type and the Uninitialized Property

A property type checks every value written to the property. A typed property without a default value is in an uninitialized state; this is not the same as being `null`. Reading an uninitialized property raises an `Error`. On a typed property that accepts `null`, if a default value is written, reading gives `null`.

```php
<?php
// Profile.php — a property type and an uninitialized property
class Profile
{
    public int $age;
    public ?string $name = null;
}

$profile = new Profile();
var_dump($profile->name);

try {
    echo $profile->age;
} catch (Error $e) {
    echo get_class($e), "\n";
}

try {
    $profile->age = 'abc';
} catch (TypeError $e) {
    echo get_class($e), "\n";
}

$profile->age = '42';
var_dump($profile->age);
```

```
NULL
Error
TypeError
int(42)
```

`name` carried `null` by default, so it could be read; `age`, never having been written, gave an `Error`. `'abc'` could not be converted to a number, so a `TypeError` arose, while `'42'` was coerced to `int` in a file without strict mode. The property type is therefore two separate promises: the type of the value written, and that it cannot be read before it is written.

#### mixed and Coercion in a Union Type

`mixed` accepts every value, `null` included. What separates it from writing no type is that it says the type was left open on purpose. In a union type, if the value fits none of the members exactly, PHP tries a coercion in a file without strict mode and chooses by rule which member to convert it to: a string that can be read as a whole number becomes `int`, and a string with a fractional part becomes `float`.

```php
<?php
// Accept.php — coercion in mixed and in an int|float union type
class Accept
{
    public static function any(mixed $value): string
    {
        return gettype($value);
    }

    public static function number(int|float $number): string
    {
        return gettype($number);
    }
}

echo Accept::any(null), ' ', Accept::any([1]), "\n";
echo Accept::number('1.5'), ' ', Accept::number('7'), ' ', Accept::number(true), "\n";
```

```
NULL array
double integer integer
```

`mixed` rejected no value. `int|float` converted the string `'1.5'` to `float` (`gettype` writes this as `double`), and the string `'7'` and the value `true` to `int`. A union type does not preserve the type of an input; it may convert it to one of the members it accepts. So whoever limits the type has also limited which conversions are accepted.

#### The static Return Type

If the return type of a method is `static`, it says that the method will return an object of whichever class it was called on. `self`, by contrast, points to the class in which the method was written. If methods that are called in a chain use `static`, the object of the subclass stays a subclass along the chain.

```php
<?php
// Query.php — the static return type keeps the subclass in a chain
class Query
{
    private array $parts = [];

    public function where(string $condition): static
    {
        $copy = clone $this;
        $copy->parts[] = $condition;
        return $copy;
    }

    public function size(): int
    {
        return count($this->parts);
    }
}

class SpecialQuery extends Query
{
}

$query = (new SpecialQuery())->where('a')->where('b');
echo get_class($query), ' ', $query->size(), "\n";
```

```
SpecialQuery 2
```

Although `where()` is written in the `Query` class, the object returned was `SpecialQuery` because it was called on `SpecialQuery`. Had the return type been written as `self`, the type would be read as `Query`, and the fact that the object at the end of the chain is a subclass would be lost at the type level. This difference matters for code that will extend a class later: being able to call the subclass's own methods at the end of the chain depends on whether the return type preserves the subclass.

#### Type Compatibility in Inheritance

When a subclass redefines a method, its types have to be compatible with the parent class. The rule is this: the return type can be narrowed (if the parent returns `iterable`, the subclass can return `array`, because every `array` is an `iterable`), while the parameter type can be widened but not narrowed. The guarantees that the parent's code expects must not weaken in the subclass.

```php
<?php
// Source.php — narrowing the return type is allowed
class Source
{
    public function provide(): iterable
    {
        return [1];
    }
}

class Narrower extends Source
{
    public function provide(): array
    {
        return [1, 2];
    }
}

echo count((new Narrower())->provide()), "\n";
```

```
2
```

Code that tries the opposite stops at the class declaration stage.

```bash
# incompatible.sh — widening the return type and narrowing the parameter type make a class undeclarable
cat > Wide.php <<'PHP'
<?php
class Base { public function provide(): array { return []; } }
class Child extends Base { public function provide(): iterable { return []; } }
PHP
cat > Narrow.php <<'PHP'
<?php
class Base { public function take(int|string $input): void {} }
class Child extends Base { public function take(int $input): void {} }
PHP
php -d display_errors=0 Wide.php; echo "wide exit=$?"
php -d display_errors=0 Narrow.php; echo "narrow exit=$?"
```

```
wide exit=255
narrow exit=255
```

Both hit the same principle. Every object placed in a variable of type `Base` promises that `provide()` will return an `array` when called; a subclass that returns `iterable` breaks this promise. Likewise, if `take()` accepts both `int` and `string`, a subclass that accepts only `int` can reject a call written with the parent's type.

#### The Measurement Core

The measurement in this lesson also uses the `reach.php` file; the core was explained line by line in the first lesson.

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

#### Adding a Type to the Parameter

The measured change is turning the untyped `add($amount)` method into `add(int $amount): float`. The consumer `main.php` leaves the variable as `null`, calls `Vat::add($amount)`, and prints the result with `var_dump`.

```php
<?php
// parameter_type.php — int is added to an untyped parameter, the consumer passes null
require_once 'reach.php';

measure('parameter-type', [
    'vat.php' => <<<'PHP'
        <?php
        class Vat
        {
            public static function add($amount) { return $amount * 1.2; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'vat.php';
        $amount = null;
        var_dump(Vat::add($amount));
        PHP,
], [
    'vat.php' => <<<'PHP'
        <?php
        class Vat
        {
            public static function add(int $amount): float { return $amount * 1.2; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/parameter-type/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
parameter-type     files=1  exit=255 output changed -> breaking
before exit=0 output=float(0)
after exit=255 output=
```

**OM26.** Before the change, `null * 1.2` quietly gave `float(0)`; once the type was added, `null` could not be converted to an `int`, so a `TypeError` arose and the exit code became 255. The error was heard at execution time, with the consumer file unchanged. The lesson's first reach is **breaking**.

#### The Bounding Measurement

Does the break come from adding a type declaration, or from the added type rejecting `null`? Making the same change with `?int` separates the second.

```php
<?php
// nullable.php — the same change, this time with ?int
require_once 'reach.php';

measure('nullable', [
    'vat.php' => <<<'PHP'
        <?php
        class Vat
        {
            public static function add($amount) { return $amount * 1.2; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'vat.php';
        $amount = null;
        var_dump(Vat::add($amount));
        PHP,
], [
    'vat.php' => <<<'PHP'
        <?php
        class Vat
        {
            public static function add(?int $amount): float { return $amount * 1.2; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/nullable/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
nullable           files=1  exit=0   output same    -> in-place
before exit=0 output=float(0)
after exit=0 output=float(0)
```

**OM27.** With `?int`, `null` still passed and `float(0)` stayed the same: a type declaration could be an in-place change. What broke was the type rejecting `null`. Another kind of input is the numeric string: in a file that does not use `strict_types`, a numeric string such as `'5'` is coerced to an `int` parameter.

```php
<?php
// numeric_string.php — the consumer passes '5', its file does not use strict_types
require_once 'reach.php';

measure('numeric-string', [
    'vat.php' => <<<'PHP'
        <?php
        class Vat
        {
            public static function add($amount) { return $amount * 1.2; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'vat.php';
        var_dump(Vat::add('5'));
        PHP,
], [
    'vat.php' => <<<'PHP'
        <?php
        class Vat
        {
            public static function add(int $amount): float { return $amount * 1.2; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/numeric-string/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
numeric-string     files=1  exit=0   output same    -> in-place
before exit=0 output=float(6)
after exit=0 output=float(6)
```

**OM28.** `'5'` became `5` through coercion, and the result stayed `float(6)` both before and after: the consumer is in place. So adding a type stops the consumer only on the inputs the type rejects; it does not reach the inputs it accepts or can coerce. The breaking results measured in this course were heard both at declaration and at call time, and at which stage they are heard is the subject of M11/K01; the course measures not the stage but whether the change reaches the consumer.

#### Summary

- Parameter, return, and property types limit the point at which a value is accepted; `?int` is `int` or `null`, and `int|string` is one of two types.
- When `int` was added to the untyped `add($amount)` method, the consumer that passes `null` got exit code 255 instead of `float(0)`; the lesson's reach is breaking.
- When the same change was made with `?int`, `null` still passed and the output stayed `float(0)`: in place.
- If the consumer passes `'5'` and its file is not in strict mode, adding the type stayed in place because of coercion, with output `float(6)`.
- Whether adding a type reaches the consumer depends on the inputs the type rejects.

#### Next Step

As classes, interfaces, traits, constants, magic methods, and types spread from one file to many, name collisions become unavoidable. The next lesson, Namespaces, asks how importing with `use` and changing a namespace affect the consumer.

## Standards and Packages

Namespaces, autoloading, common interface standards, the dependency manager and dependency injection — arrangements that either gather a change into a single wiring point or carry it to every user.

### Namespaces

Source: https://academia.sh/en/courses/php-object-model/namespaces

namespace, use and aliases with fully qualified names; two same-named classes kept apart by a namespace, while the same collision in code without namespaces stops the program; an unqualified function call falling back to the global function.

In the Type Declarations lesson the change came from inside the definition: a type was added to a parameter and the old call paid for it. In this lesson the change comes from outside the definition. Someone else adds a class that carries the same name as our class. The question that runs through the course does not change: **where does this change reach?** The answer is one of four: **in place**, **at the seam**, **breaking**, **implicit**.

M09/K02 looked for the place where a name is bound: in the declaration, at the point of use, or in the value. A namespace adds a prefix to the place where that binding is made. This lesson measures how many files are edited when the binding changes, and what the unedited code experiences.

#### Namespaces, use and the Fully Qualified Name

A namespace is a scope that gives names a prefix. A class in a file opened with `namespace Shop;` carries the name `Shop\Record`, not `Record`. This long prefixed form is called the **fully qualified name**. `use` makes a fully qualified name referable by its short name inside the file; `as` gives an alias to one of two classes that share the same short name.

```php
<?php
// names.php — namespace, fully qualified name and alias
namespace Shop {
    class Record {}
}
namespace External {
    class Record {}
}
namespace {
    use Shop\Record;
    use External\Record as ExternalRecord;

    echo Record::class, "\n";
    echo ExternalRecord::class, "\n";
    echo \External\Record::class, "\n";
    echo get_class(new ExternalRecord()), "\n";
}
```

```
Shop\Record
External\Record
External\Record
External\Record
```

The two classes share a short name but have different fully qualified names. The name `Record` was bound to `Shop\Record`, the alias `ExternalRecord` to `External\Record`; the third line, which starts with a backslash, writes the full name without any `use`. `get_class` returns the real name of the object in fully qualified form. A namespace is not a folder but a name prefix: it does not say in which file the class lives.

An unqualified class name written inside a namespace is taken to belong to that namespace. PHP's own classes live in the global namespace, so while a namespace is open they are reached with the full name or with `use`.

```php
<?php
// root.php — inside a namespace an unqualified class name does not fall back to the global one
namespace Shop;

echo Exception::class, "\n";
echo \Exception::class, "\n";

try {
    new Exception('x');
} catch (\Error $error) {
    echo get_class($error), "\n";
}
echo get_class(new \Exception('x')), "\n";
```

```
Shop\Exception
Exception
Error
Exception
```

When `Exception` was written, PHP read it as `Shop\Exception`; since no such class exists, `new` threw an `Error` and `catch` caught it. The name with a leading backslash selected the global class. The rule for classes is this: a class name that cannot be found in the namespace does **not** fall back to the global one. The rule for functions is different, and that difference is exactly the bounding measurement of this lesson.

#### Name Resolution: Unqualified, Qualified, Fully Qualified

PHP sees a name in three forms, and each is resolved differently. An **unqualified** name contains no backslash (`Money`). A **qualified** name contains one but does not start with one (`Sub\Money`). A **fully qualified** name starts from the global namespace and begins with a backslash (`\Money`). The resolution order is short. A fully qualified name stays as it is. An unqualified name is first checked against the imports made with `use`; if it was not imported, it is appended to the current namespace. If the first part of a qualified name is a `use` alias, that part is replaced with the name the alias points to; otherwise the name is again written under the current namespace. The `namespace\Money` form explicitly selects the current namespace.

This resolution happens at compile time and does not require the class to exist. `::class` gives only the resolved name, so in the driver below the results of five forms can be seen side by side without defining any class.

```php
<?php
// resolution.php — five ways of writing a name and their resolved names
namespace Shop\Order;

use Shop\Tools as A;

echo 'unqualified     : ', Money::class, "\n";
echo 'qualified       : ', Sub\Money::class, "\n";
echo 'aliased         : ', A\Money::class, "\n";
echo 'fully qualified : ', \Money::class, "\n";
echo 'namespace\ prefix: ', namespace\Money::class, "\n";
```

```
unqualified     : Shop\Order\Money
qualified       : Shop\Order\Sub\Money
aliased         : Shop\Tools\Money
fully qualified : Money
namespace\ prefix: Shop\Order\Money
```

Four of the five lines went to the current namespace or to the place the alias points to; only the name starting with a backslash went to the global namespace. This also says what happens when the code is moved to another namespace: unqualified and qualified names move together with the namespace of the file that carries them, while fully qualified names stay where they are. For classes the resolution ends here. For functions and constants there is one more step: an unqualified name that cannot be found in the current namespace falls back to the global one. The bounding measurement measures the consequence of this difference.

#### Grouped use, use function and use const

A file can contain several `use` statements, and names that come from the same namespace can be gathered on one line with curly braces. Plain `use` is enough for classes; functions and constants need separate keywords: `use function` and `use const`. Plain `use` does not import a function or a constant.

```php
<?php
// imports.php — grouped use, use function and use const
namespace Shop\Tools {
    const VAT = 20;
    function taxed(int $price): int { return $price + intdiv($price * VAT, 100); }
    class Round {}
    class Money {}
}
namespace {
    define('GENERAL', 'global');
}
namespace App {
    use Shop\Tools\{Round, Money as Amount};
    use function Shop\Tools\taxed;
    use const Shop\Tools\VAT;

    echo taxed(100), ' ', VAT, "\n";
    echo Round::class, ' ', Amount::class, "\n";
    echo GENERAL, "\n";
}
```

```
120 20
Shop\Tools\Round Shop\Tools\Money
global
```

The first line shows two imports: the function `taxed` and the constant `VAT` were used without writing the namespace. On the second line the grouped `use` imported two classes in one statement, and the class `Money` was given the alias `Amount` inside the group. The third line shows that a global constant can be read from inside a namespace without any `use`: constants fall back to the global namespace just as functions do. Classes do not.

#### Where the namespace Declaration Goes, and Independence from the Directory

The `namespace` declaration must be the first statement of the file; only `declare` may come before it. Even an `echo` written before it stops the file at compile time. The driver below tries this in a separate PHP process and prints only the exit code. The same driver shows a second thing as well: the class in the `Shop\Model` namespace is defined in a file inside the `deep/inner` directory, which has nothing to do with its name, and it still works.

```php
<?php
// place.php — namespace must be the first statement; the directory does not determine the namespace
$dir = 'measure/place/deep/inner';
mkdir($dir, 0777, true);
file_put_contents("$dir/class.php", "<?php\nnamespace Shop\\Model;\nclass Product {}\n");
file_put_contents('measure/place/late.php', "<?php\necho 'before';\nnamespace Shop;\n");

require "$dir/class.php";
$file = (new ReflectionClass('Shop\Model\Product'))->getFileName();
echo \Shop\Model\Product::class, ' ', basename($file), "\n";

exec('php -d display_errors=0 measure/place/late.php 2>/dev/null', $lines, $code);
echo "namespace not first: exit=$code\n";
```

```
Shop\Model\Product class.php
namespace not first: exit=255
```

The first line proves independence from the directory: the class name is `Shop\Model\Product`, while its file is `measure/place/deep/inner/class.php`. PHP does not match the two; a namespace is only a prefix declared inside the file. The second line shows that the process ended with 255 because the `namespace` declaration came after the `echo`. The mapping between a namespace and a directory is the job not of the language but of a convention. The next lesson sets up that convention: the autoloader takes on the task of matching the namespace prefix to a directory.

#### The Measurement Setup

The measurement is done with `reach.php`. The file carries the `measure()` function; `measure()` builds the same program before and after the change, runs both, and asks for the reach in order: if the later run exits with a non-zero code 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**. There are two conditions: the earlier version must exit with 0, and the consumer file `main.php` is not edited. The code blocks in the lessons below only mention the file; the driver of this lesson pulls it in with `require_once`. The file is only written to disk, without an output block.

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

#### The Collision: Code With and Without a Namespace

In the `namespace` measurement, `ours.php` defines the class `Record` in the `App` namespace, and its `source()` method returns `app`. The external code adds to `external.php` a `Record` of the same name inside its own namespace, `External`. The consumer calls our class with `use App\Record;`. The counterpart is `global-name`: both classes stand in the global namespace, without any namespace.

```php
<?php
// namespace.php — a same-named class with a namespace and without one
require_once 'reach.php';

// 8. External code adds a class with the same name as ours, in its own namespace
$reaches[] = measure('namespace', [
    'ours.php' => <<<'PHP'
        <?php
        namespace App;
        class Record
        {
            public function source(): string { return 'app'; }
        }
        PHP,
    'external.php' => <<<'PHP'
        <?php
        namespace External;
        class Log {}
        PHP,
    'main.php' => <<<'PHP'
        <?php
        use App\Record;
        require 'ours.php';
        require 'external.php';
        echo (new Record())->source(), "\n";
        PHP,
], [
    'external.php' => <<<'PHP'
        <?php
        namespace External;
        class Log {}
        class Record
        {
            public function source(): string { return 'external'; }
        }
        PHP,
]);

$reaches[] = measure('global-name', [
    'ours.php' => "<?php\nclass Record\n{\n    public function source(): string { return 'app'; }\n}\n",
    'external.php' => "<?php\nclass Log {}\n",
    'main.php' => "<?php\nrequire 'ours.php';\nrequire 'external.php';\necho (new Record())->source(), \"\\n\";\n",
], [
    'external.php' => "<?php\nclass Log {}\nclass Record\n{\n    public function source(): string { return 'external'; }\n}\n",
]);

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

```
namespace          files=1  exit=0   output same    -> in-place
global-name        files=1  exit=255 output changed -> breaking
app -> app
```

The first line is `namespace`: the external code brought the same name, one file changed, the exit code was 0, and the output was `app` before and stayed `app`. Because an empty seam list was given, the reach is **in place**: the change stayed inside `external.php` and did not reach the consumer. The namespace confined the collision inside the definition.

The second line is the counterpart: when the same single-file addition was made without a namespace, the program stopped and exited with 255. The reach is **breaking**. The difference is not in the number of files; in both cases one file changed. The difference is in where the name lives.

For a namespace to do its job, the name of the namespace itself must not collide either. This is why the first segment of a namespace is usually a name that belongs to the project or to the team that writes it, and the subsegments follow the directory structure: `Shop\Model`, `Shop\Service`. When two separate libraries choose the same first segment, the namespace stops being a protection. Choosing the prefix is a matter of convention, not something the language checks.

**SP1.** This lesson treats the namespace only as a matter of name prefixes. How files are found and loaded belongs to the Autoloading lesson; here every class is pulled in by hand with `require`.

#### The Bounding Measurement

A namespace protected the classes, but it does not protect everything. For an unqualified **function** call, PHP first looks in the namespace where the call stands, and if the function is not there it falls back to the global function. In the driver below, the consumer calls `strlen` inside `namespace App;`. The `helper.php` it includes at first carries only `greet()`; in the change, a `strlen` function also enters the same namespace. In the second measurement the call is written as `\strlen`.

```php
<?php
// function.php — an unqualified function call falls back to the global function
require_once 'reach.php';

$helper = <<<'PHP'
    <?php
    namespace App;
    function greet(): string { return 'hello'; }
    PHP;
$extended = $helper . "\n" . 'function strlen(string $text): int { return 0; }' . "\n";
$main = "<?php\nnamespace App;\nrequire 'helper.php';\necho %s('apple'), \"\\n\";\n";

measure('function-fallback', ['helper.php' => $helper, 'main.php' => sprintf($main, 'strlen')], ['helper.php' => $extended]);
measure('root-call', ['helper.php' => $helper, 'main.php' => sprintf($main, '\strlen')], ['helper.php' => $extended]);

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

```
function-fallback  files=1  exit=0   output changed -> implicit
root-call          files=1  exit=0   output same    -> in-place
5 -> 0
```

The first line is `function-fallback`: the call `strlen('apple')` found the function `App\strlen` and printed `0` instead of `5`. The program did not stop and the exit code stayed 0; the output changed, so the reach is **implicit**. The consumer was never edited, and the change reached it all the same: an unqualified call does not say which `strlen` is meant. The second line is `root-call`: when the same call was written as `\strlen`, PHP chose the global function without looking at the namespace, and the reach stayed **in place**. A leading backslash explicitly selects the global namespace.

A namespace protected against a class collision, but it did not protect a function call on its own. The same mechanism either protects or does not, depending on how the call is written. What is measured is, once again, not the definition itself but what the consumer touches.

#### Summary

- The reach of this lesson is **in place**. In the `namespace` measurement, external code brought a same-named class in its own namespace, one file changed, and the output stayed `app`.
- Its counterpart `global-name` is **breaking**: the same addition without a namespace stopped the program with exit code 255. One file changed in both cases; the difference was the place where the name lives.
- `namespace` gives names a prefix, `use` makes a fully qualified name referable by its short name, and `as` gives an alias to colliding short names.
- The bounding measurement: an unqualified function call falls back to the global one. The `strlen` call aimed at `App\strlen` went `5` → `0` and the reach was **implicit**; with `\strlen` it was **in place**.
- A namespace is not a folder but a name prefix: it does not say in which file the class lives. Unqualified, qualified and fully qualified names are resolved separately; `use function` and `use const` import a function and a constant; the `namespace` declaration is the first statement of the file.

#### Next Step

In this lesson, finding the class was done by hand with `require`. If a class file moves to another root directory, how many files change, and is the consumer affected? The Autoloading lesson answers this question by measuring the loader as a single seam.

### Autoloading

Source: https://academia.sh/en/courses/php-object-model/autoloading

An autoloader built with spl_autoload_register; mapping a namespace prefix to a directory and PSR-4; the number of files not deciding the reach when a class file moves; a move made without updating the loader stopping the program; the mapping depending on letter case in the file name.

In the Namespaces lesson every class was pulled in by hand with `require`. That means the consumer knows in which file a class lives; if the class file moves, that knowledge goes stale on every `require` line. In this lesson the same knowledge is gathered in one place: the **autoloader**. The question is again the one asked throughout the course: **where does this change reach?**

M09/K02 looked for the place where a name is bound. The loader is the place where the binding between a name and a file path is made. This lesson measures how many files are edited when that binding changes, and what the unedited code experiences.

#### Manual Loading and Its Cost

Before loaders, every class file was pulled in by hand with `require_once`. This method has two costs. First, the consumer file must know the path of every class it uses; if a path changes, every `require_once` line goes stale. Second, the lines run whether or not the class is used: a program that needs only one class loads all four. Because classes themselves need other classes, these lines turn into a chain, and the order in which the chain is built also becomes the consumer's burden.

The second half of the cost can be counted. The driver below writes four empty class files and builds two separate programs: one pulls in the four files by hand, the other registers a loader. Both use only the class `A` and print at the end how many files were loaded.

```php
<?php
// chain.php — manual loading loads every file, the loader loads only what is used
mkdir('measure/chain/src', 0777, true);
foreach (['A', 'B', 'C', 'D'] as $name) {
    file_put_contents("measure/chain/src/$name.php", "<?php\nclass $name {}\n");
}

$manual = <<<'PHP'
    <?php
    require_once 'src/A.php';
    require_once 'src/B.php';
    require_once 'src/C.php';
    require_once 'src/D.php';
    new A();
    echo count(get_included_files()) - 1, "\n";
    PHP;
$automatic = <<<'PHP'
    <?php
    spl_autoload_register(fn (string $class) => require __DIR__ . "/src/$class.php");
    new A();
    echo count(get_included_files()) - 1, "\n";
    PHP;
file_put_contents('measure/chain/manual.php', $manual);
file_put_contents('measure/chain/auto.php', $automatic);

foreach (['manual', 'auto'] as $name) {
    echo str_pad($name, 7), exec("php measure/chain/$name.php"), " loaded\n";
}
```

```
manual 4 loaded
auto   1 loaded
```

The manually written version loaded four files although the program asked only for `A`. The version with a loader loaded one file: `B`, `C` and `D` were never requested and never opened. The difference grows with the number of classes; in an application with three hundred classes, each request reads only the files of the classes it touches.

#### When the Loader Is Called

`spl_autoload_register` registers a function as a loader. When PHP meets a class name it does not know, it calls this function with the fully qualified name of the class; the function is responsible for loading the file. If the class is already defined, or if the call skips the loader, the function does not run.

```php
<?php
// observe.php — the loader is called only for an unknown class
spl_autoload_register(function (string $class): void {
    echo "requested: $class\n";
});

var_dump(class_exists('Shop\Model\Product'));
var_dump(class_exists('Shop\Model\Product', false));
echo "done\n";
```

```
requested: Shop\Model\Product
bool(false)
bool(false)
done
```

The first `class_exists` called the loader with the fully qualified name. In this example the loader loaded no file, so the class still could not be found and the result was `false`. The second call skipped the loader with its second argument: the function did not run at all. A loader is not a declaration but a callback; PHP calls it only for a missing class and only at that moment.

#### Which Operations Call the Loader

The loader is called wherever PHP has to resolve a class name and does not yet know the class. Roughly, these are the operations that really need the class to exist: `new`, a static call, a class constant, `class_exists`. Operations that do not require the class to exist do not call the loader: `instanceof`, `::class` and a parameter type declaration. The driver below registers the loader as a function that loads no class, so after each operation it becomes visible whether the loader was called. A class that cannot be found throws an `Error`; the driver catches the error and carries on.

```php
<?php
// trigger.php — which operations call the loader
spl_autoload_register(function (string $class): void {
    echo "  loader: $class\n";
});

function probe(string $name, callable $action): void
{
    echo "$name\n";
    try {
        $action();
    } catch (Error) {
    }
}

probe('new', fn () => new One());
probe('static call', fn () => Two::run());
probe('class constant', fn () => Three::CONSTANT);
probe('class_exists', fn () => class_exists('Four'));
probe('instanceof', fn () => new stdClass() instanceof Five);
probe('::class', fn () => Six::class);
probe('type declaration', function () {
    (fn (Seven $parameter) => 1)(1);
});
```

```
new
  loader: One
static call
  loader: Two
class constant
  loader: Three
class_exists
  loader: Four
instanceof
::class
type declaration
```

The first four operations called the loader and received the class name. The last three did not. `instanceof` knows that if the compared class does not exist, the object cannot be of that class, so it has no need to load it. `::class` returns only the name resolved at compile time. A type declaration performs its check with the class name of the object; if a value of the wrong type arrives it throws a `TypeError`, but the loader is again not called. The consequence is this: a class is loaded only the first time it is really used. That also means that in a request where a code path never runs, the classes on that path are never loaded.

#### Multiple Loaders

`spl_autoload_register` can be called more than once. Registered loaders are called in registration order, and the chain stops as soon as the class is defined. If the third argument is `true`, the loader is added to the front of the queue. An application can carry the loaders of its dependencies in addition to its own; for this reason each loader should handle only its own prefix and leave a name it does not recognize alone, without raising an error.

```php
<?php
// order.php — loaders are called in registration order, the chain stops when the class is found
spl_autoload_register(function (string $class): void { echo "first: $class\n"; });
spl_autoload_register(function (string $class): void {
    echo "second: $class\n";
    if ($class === 'Target') {
        class_alias(stdClass::class, 'Target');
    }
});
spl_autoload_register(function (string $class): void { echo "third: $class\n"; });
spl_autoload_register(function (string $class): void { echo "front: $class\n"; }, true, true);

echo "-- Target\n";
class_exists('Target');
echo "-- Missing\n";
class_exists('Missing');
```

```
-- Target
front: Target
first: Target
second: Target
-- Missing
front: Missing
first: Missing
second: Missing
third: Missing
```

`front` was registered last, but because `true` moved it to the front, it was called first. For `Target` the chain stopped at the second loader: it defined the name `Target`, and `third` was never called. For `Missing`, no loader defined the class, so all four were called in order and the class still could not be found. Every loader call has a cost; this is why it matters that a loader which does not recognize the prefix returns on its first line.

#### Mapping a Prefix to a Directory

The loader's job is to turn the fully qualified name into a file path. **PSR-4**, the standards recommendation of the PHP community, binds this to a short rule: a prefix of the namespace corresponds to a base directory, the namespace parts left after the prefix become subdirectories, the last part is the file name, and `.php` is appended to the end. If the prefix `Shop\` is mapped to the directory `src/`, the class `Shop\Model\Product` is loaded from the file `src/Model/Product.php`.

**SP2.** This lesson builds the loader with a single prefix and a single base directory. Mappings with several prefixes or fallback directories, and old class names that carry no namespace, are not measured.

The mapping is read in four steps: the prefix is removed, the remaining relative name is taken, the namespace separator is turned into the directory separator, and `.php` is appended. The driver below prints these four steps separately on the name `Shop\Model\Product`.

```php
<?php
// psr4.php — the PSR-4 mapping step by step
$prefix = 'Shop\\';
$base = 'src/';
$class = 'Shop\Model\Product';

echo "full name     : $class\n";
$relative = substr($class, strlen($prefix));
echo "relative name : $relative\n";
$path = str_replace('\\', '/', $relative);
echo "path          : $path\n";
echo "file          : $base$path.php\n";
```

```
full name     : Shop\Model\Product
relative name : Model\Product
path          : Model/Product
file          : src/Model/Product.php
```

Each line derives from the previous one, and none of them looks at the file system: the mapping is purely a string operation. Whether the class is really defined in the file can be known only after `require`. For this reason a good loader checks that the file exists before `require`; if the file does not exist it raises no error and gives the next loader in the chain a chance. The `loader.php` of this lesson skips that check to keep the measurement plain.

#### The Measurement Setup

As in the previous lesson, `reach.php` carries the `measure()` function: 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;
}
```

#### The Move: Three Files, at the Seam

In the `autoloading` measurement, the class `Shop\Model\Product` lives in the file `src/Model/Product.php`, and `loader.php` looks for it under `src/`. The class moves to the base directory `source/`. Three files change: the old file is deleted (its value is `null`), the new one is added, and the loader is updated to point at the new directory. The fourth argument is the seam list and names `loader.php` as the seam. The counterpart is `no-autoloader`: the same move, made without updating the loader.

The `loader.php` in the baseline builds the mapping in three steps. If the incoming name starts with `Shop\`, it takes part; otherwise it leaves the name to another loader. In the remaining part after the prefix, `Model\Product`, the backslashes are turned into forward slashes and `.php` is appended. The resulting path, `Model/Product.php`, is searched for under the base directory `src/`. The base directory stands in the loader as a single string; all the knowledge of where the class files are loaded from is gathered there. This is why the move shows up only in this string, not in the consumer.

```php
<?php
// loading.php — a class file moves, and the loader is updated or not
require_once 'reach.php';

// 9. A class file moves to another root directory
$reaches[] = measure('autoloading', [
    'src/Model/Product.php' => <<<'PHP'
        <?php
        namespace Shop\Model;
        class Product
        {
            public function name(): string { return 'pen'; }
        }
        PHP,
    'loader.php' => <<<'PHP'
        <?php
        spl_autoload_register(function (string $class): void {
            $prefix = 'Shop\\';
            if (str_starts_with($class, $prefix)) {
                require 'src/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
            }
        });
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'loader.php';
        echo (new Shop\Model\Product())->name(), "\n";
        PHP,
], [
    'src/Model/Product.php' => null,
    'source/Model/Product.php' => <<<'PHP'
        <?php
        namespace Shop\Model;
        class Product
        {
            public function name(): string { return 'pen'; }
        }
        PHP,
    'loader.php' => <<<'PHP'
        <?php
        spl_autoload_register(function (string $class): void {
            $prefix = 'Shop\\';
            if (str_starts_with($class, $prefix)) {
                require 'source/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
            }
        });
        PHP,
], ['loader.php']);

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

$before = read_files('measure/autoloading/before');
measure('no-autoloader', $before, [
    'src/Model/Product.php' => null,
    'source/Model/Product.php' => $before['src/Model/Product.php'],
]);
```

```
autoloading        files=3  exit=0   output same    -> at-seam
pen -> pen
no-autoloader      files=2  exit=255 output changed -> breaking
```

The first line is `autoloading`: three files changed, the exit code stayed 0, and the output was `pen` before and stayed `pen`. Because the output did not change and one of the edited files is in the seam list, the reach is **at the seam**: the change was absorbed at a single point and `main.php` was not edited. The second line is the counterpart: the same move, made without updating the loader, ended with two files but exited with 255, and the reach became **breaking**; the loader was still looking for the class under `src/`.

The consequence is this: the number of files does not decide the reach. The move that changed three files was trouble-free, and the move that changed two files stopped the program. What decides is whether the consumer was edited, and whether the single point that holds the binding is up to date.

#### The Bounding Measurement

The mapping carries the letters of the class name to the file name as they are. For the class `Product`, the loader asks for the file `Product.php`. What happens if the class file is renamed in lowercase as `product.php`? The driver measures this change on the same baseline, without touching the loader.

```php
<?php
// case.php — letter case in the file name is part of the mapping
require_once 'reach.php';

$before = read_files('measure/autoloading/before');
measure('lowercase', $before, [
    'src/Model/Product.php' => null,
    'src/Model/product.php' => $before['src/Model/Product.php'],
]);
```

```
lowercase          files=2  exit=255 output changed -> breaking
```

The reach became **breaking**: two files changed and the exit code was 255. The loader asked for `Product.php`, and the directory held only `product.php`. This is a consequence of the file system, not of the language: the file system of the sandbox distinguishes upper and lower case. On a file system that does not distinguish case, the same change may work; the lesson did not measure that and prints only the sandbox result. For this reason the mapping also requires the file name to match the class name letter for letter.

#### Summary

- The reach of this lesson is **at the seam**. In the `autoloading` measurement, the class file moved to another root directory, three files changed, the output stayed `pen`, and the change was absorbed at the point `loader.php`.
- Its counterpart `no-autoloader` is **breaking**: a two-file move made without updating the loader exited with 255. The number of files does not decide the reach.
- The loader is called with a fully qualified name for an unknown class; the PSR-4 prefix maps to a directory, the remaining namespace parts become subdirectories, and the last part becomes the file name.
- The loader is called only by operations that need the class to exist (`new`, a static call, a class constant, `class_exists`); `instanceof`, `::class` and a type declaration do not call it. Several loaders run in registration order, and the chain stops when the class is found.
- The bounding measurement: when the file was renamed `product.php`, the reach in the sandbox was **breaking**; this is a consequence of the file system.

#### Next Step

The loader gathered in one place where a class comes from. The inside of the class, however, can still change behind an interface: if the consumer depends on an interface and the implementation changes, how many places are edited? The Common Interface Standards lesson measures this.

### Common Interface Standards

Source: https://academia.sh/en/courses/php-object-model/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.

### The Dependency Manager

Source: https://academia.sh/en/courses/php-object-model/dependency-manager

The manifest file, the version constraint and the lock file; a minor version of an unlocked dependency silently changing the output; the lock file pinning the version; the constraint guarding the major version boundary; semantic versioning being a promise.

In the Common Interface Standards lesson the implementation was a class we wrote ourselves. In this lesson the implementation is a package someone else publishes, and the change happens not in your repository but in the release version of the package. The question is the same: **where does this change reach?**

A **package manager** holds three things together: the **manifest file**, which says which packages you depend on, the **version constraint**, which says which range of versions you accept, and the **lock file**, which records the exact versions that were resolved. PHP's common package manager works with these same three parts. This lesson does not run the manager itself; it never reaches the network. The measurement is made with a small resolver that imitates the same three parts in a local directory.

**SP4.** The resolver in the measurement understands a single constraint of the form `^1.4`: greater than or equal to `1.4`, less than `2.0.0`. The comparison of `^` and `~` is modeled by a separate function. The narrower rule a real manager applies to zero major versions (`0.x`), resolution across several packages, and the dependencies of dependencies are not modeled.

#### 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;
}
```

#### An Unlocked Dependency: Implicit

In the `unlocked-version` observation, `dependencies.json` says `{"dates": "^1.4"}`, that is, it accepts version 1.4 and above of the `Dates` package, below 2.0.0. `resolver.php` picks the highest version in the packages directory that fits the constraint and loads the file `source.php`. `main.php` formats a date with `Dates::format(2026, 10, 15)`. In the baseline only `packages/dates-1.4.0` exists and the resolver picks it. The change: version 1.5.0 of the package is released. No file in your repository is edited; only `packages/dates-1.5.0/source.php` is added. There is no lock file.

```php
<?php
// unlocked.php — a dependency without a lock file takes the new minor version
require_once 'reach.php';

// 11. A new minor version of the dependency is released; there is no lock file
$reaches[] = measure('unlocked-version', [
    'dependencies.json' => '{"dates": "^1.4"}',
    'packages/dates-1.4.0/source.php' => <<<'PHP'
        <?php
        class Dates
        {
            public static function format(int $year, int $month, int $day): string
            {
                return sprintf('%04d-%02d-%02d', $year, $month, $day);
            }
        }
        PHP,
    'resolver.php' => <<<'PHP'
        <?php
        function pickVersion(string $package, string $constraint): string
        {
            $lower = ltrim($constraint, '^');
            $upper = ((int) explode('.', $lower)[0] + 1) . '.0.0';
            $candidates = [];
            foreach (glob("packages/$package-*") as $dir) {
                $version = substr($dir, strlen("packages/$package-"));
                if (version_compare($version, $lower, '>=') && version_compare($version, $upper, '<')) {
                    $candidates[] = $version;
                }
            }
            usort($candidates, 'version_compare');
            return end($candidates);
        }
        $requested = json_decode(file_get_contents('dependencies.json'), true);
        $lock = is_file('lock.json') ? json_decode(file_get_contents('lock.json'), true) : [];
        foreach ($requested as $package => $constraint) {
            require "packages/$package-" . ($lock[$package] ?? pickVersion($package, $constraint)) . '/source.php';
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'resolver.php';
        echo Dates::format(2026, 10, 15), "\n";
        PHP,
], [
    'packages/dates-1.5.0/source.php' => <<<'PHP'
        <?php
        class Dates
        {
            public static function format(int $year, int $month, int $day): string
            {
                return sprintf('%02d.%02d.%04d', $day, $month, $year);
            }
        }
        PHP,
]);

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

```
unlocked-version   files=1  exit=0   output changed -> implicit
2026-10-15 -> 15.10.2026
dependencies.json, main.php, packages/dates-1.4.0/source.php, resolver.php
```

The reach is **implicit**: one file was added and the exit code stayed 0, but the output went from `2026-10-15` to `15.10.2026`. Not a single line was edited in your repository, the program did not crash, and the date format changed. The resolver picked 1.5.0 as the highest version that fits the constraint; this happened not because the constraint forbade it but because it allowed it. The last line is the file list of the baseline: `lock.json` is not in the list.

#### The Lock File: In Place

The counterpart is `locked`: the same release, but the baseline carries a `lock.json` file, and the file pins the `dates` package to version `1.4.0`. If the lock file has an entry, the resolver loads that version without looking at the constraint. The change is again the release of version 1.5.0 of the package.

```php
<?php
// locked.php — the lock file keeps the pinned version
require_once 'reach.php';

$base = read_files('measure/unlocked-version/before');
$base['lock.json'] = '{"dates": "1.4.0"}';
$after = read_files('measure/unlocked-version/after');
measure('locked', $base, [
    'packages/dates-1.5.0/source.php' => $after['packages/dates-1.5.0/source.php'],
]);

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

```
locked             files=1  exit=0   output same    -> in-place
2026-10-15 -> 2026-10-15
```

The reach is **in place**: one file was added, the exit code stayed 0, and the output stayed `2026-10-15`. In the measurement without a lock file the same release had changed the output; here the lock kept the resolver at 1.4.0. The difference is a single file: the decision to pin stands in the lock file, and the new version is taken only when that file is updated.

#### Two Ways to Write a Constraint: Caret and Tilde

A constraint can be written with two different signs, and the two do not say the same thing. The `^` sign pins the leftmost nonzero part of the version: `^1.4.0` accepts 1.4.0 and above, below 2.0.0. The `~` sign looks at the number of parts written. The three-part `~1.4.0` accepts 1.4.0 and above only below 1.5.0; the two-part `~1.4` gives the same range as `^1.4`. The helper file that computes the range is below; the next two measurements use it. This is a source, not a measurement, so it has no output block.

```php
<?php
// version.php — reads ^ and ~ constraints as ranges
function bounds(string $constraint): array
{
    $sign = $constraint[0];
    $parts = array_map('intval', explode('.', substr($constraint, 1)));
    $lower = implode('.', array_pad($parts, 3, 0));
    if ($sign === '^' || count($parts) < 3) {
        $upper = ($parts[0] + 1) . '.0.0';
    } else {
        $upper = $parts[0] . '.' . ($parts[1] + 1) . '.0';
    }
    return [$lower, $upper];
}

function highest(string $constraint, array $versions): ?string
{
    [$lower, $upper] = bounds($constraint);
    $fits = array_filter(
        $versions,
        fn(string $v) => version_compare($v, $lower, '>=') && version_compare($v, $upper, '<')
    );
    usort($fits, 'version_compare');
    return $fits === [] ? null : end($fits);
}
```

The same five releases are tested with three constraints. In the output, the first column is the constraint and what follows is the selected version.

```php
<?php
// distinction.php — the same releases give different versions under three constraints
require_once 'version.php';

$releases = ['1.4.0', '1.4.7', '1.5.0', '1.9.2', '2.0.0'];
foreach (['^1.4.0', '~1.4.0', '~1.4'] as $constraint) {
    printf("%-7s -> %s\n", $constraint, highest($constraint, $releases));
}
```

```
^1.4.0  -> 1.9.2
~1.4.0  -> 1.4.7
~1.4    -> 1.9.2
```

All three constraints start at 1.4.0, but `~1.4.0` stays within patch versions and stops at 1.4.7; `^1.4.0` also takes minor versions and climbs to 1.9.2. Dropping one part, that is, writing `~1.4` instead of `~1.4.0`, brings the constraint back to the width of `^`. None of them takes 2.0.0. The measurement shows that the risk in the `unlocked-version` observation can be narrowed by the choice of a constraint; but the risk does not disappear, it only descends to patch versions.

#### A Team in Four Steps

A flow explains why the lock file goes into your repository. The manifest says `^1.4.0`, the package is at 1.4.0, and there is no lock file yet. There are two people on the team: you and a friend. The flow uses no file system; it keeps the manifest, the lock entry and the release list of the package in three variables.

```php
<?php
// flow.php — when the lock file is in the repository, everyone gets the same version
require_once 'version.php';

function install(string $constraint, array $releases, ?string $lock): array
{
    if ($lock !== null) {
        return [$lock, $lock, 'from lock'];
    }
    $version = highest($constraint, $releases);
    return [$version, $version, 'resolved'];
}

$constraint = '^1.4.0';
$releases = ['1.4.0'];

[$version, $lock, $source] = install($constraint, $releases, null);
printf("1 first install      %s  %s\n", $version, $source);

$releases[] = '1.4.7';
[$version, , $source] = install($constraint, $releases, $lock);
printf("2 friend, locked     %s  %s\n", $version, $source);
[$version, , $source] = install($constraint, $releases, null);
printf("2 friend, unlocked   %s  %s\n", $version, $source);

[$version, $lock, $source] = install($constraint, $releases, null);
printf("3 update             %s  %s\n", $version, $source);

[$version, , $source] = install($constraint, $releases, $lock);
printf("4 friend again       %s  %s\n", $version, $source);
```

```
1 first install      1.4.0  resolved
2 friend, locked     1.4.0  from lock
2 friend, unlocked   1.4.7  resolved
3 update             1.4.7  resolved
4 friend again       1.4.7  from lock
```

At the first install there was no lock, the resolver picked 1.4.0, and the lock entry became `1.4.0`. When the package released 1.4.7, your friend, who took the lock from the repository, again installed 1.4.0; without the lock they would have installed 1.4.7, and two versions would have run on two machines. The update is the step that knowingly ignores the lock: the resolver picked 1.4.7 and rewrote the lock entry. When your friend took this new entry from the repository, they met you at 1.4.7.

This is the reason the lock file goes into the repository: if the decision to pin is not distributed with the code, every machine does its own resolution. Updating is also a separate edit; the difference in the lock file shows, within a commit, which versions changed.

#### The Three Parts of Semantic Versioning

The parts that `^` and `~` distinguish are each a promise. The **major** part increases on a change that is not backward compatible. The **minor** part increases when a backward-compatible feature is added. The **patch** part increases on a backward-compatible bug fix. `^` trusts the first two of this promise: it assumes that your code will keep working as long as the major part stays fixed. `~1.4.0` trusts only the third.

In the `unlocked-version` measurement, the move from 1.4.0 to 1.5.0 was a minor part increase, and the promise would have required the output not to change. Since the date format changed, either the package did not keep its promise or your code depended on something the promise does not cover, in this case the text of the output. This distinction matters in practice: if the promise was broken, the problem is reported to the owner of the package and the constraint is narrowed; if it came from your code, the fix is made in your repository. In both cases you first need to know which one it is, and for that you need to know when the change arrived. The resolver cannot know which one it is. It does not need to: the lock file does not remove this uncertainty, but it makes certain that the change arrives when you decide and with your approval.

This is why the two decisions are made separately. The constraint says in which range you expect your code to work, and it rarely changes. The lock file says which version in that range you use today, and it changes with every update. If you choose the first wrongly, the lock file protects you; if you do not put the lock file in the repository, no constraint keeps you on the same version. The bounding measurement tests what the constraint still guards against when there is no lock.

#### The Bounding Measurement

A constraint guards against a major version change. This measurement confirms it: there is no lock file, the baseline is the `unlocked-version` baseline, and the released package is 2.0.0. The `^1.4` constraint leaves 2.0.0 out.

```php
<?php
// major-version.php — the constraint guards against a major version change
require_once 'reach.php';

$base = read_files('measure/unlocked-version/before');
measure('major-version', $base, [
    'packages/dates-2.0.0/source.php' => <<<'PHP'
        <?php
        class Dates
        {
            public static function format(int $year, int $month, int $day): string
            {
                return "$day/$month/$year";
            }
        }
        PHP,
]);

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

```
major-version      files=1  exit=0   output same    -> in-place
2026-10-15 -> 2026-10-15
```

The reach is **in place**: 2.0.0 was released, the resolver again picked 1.4.0, and the output stayed `2026-10-15`. The `^1.4` constraint guards against a major version change; but it does not guard against a behavior change in a minor version, as the `unlocked-version` measurement showed. That a minor version will not change the output is a promise called **semantic versioning**: the owner of the package gives it, and the resolver does not check it. If the promise is kept, the major version boundary is enough; if it is not, only the lock file protects.

#### Summary

- The reach of this lesson is **implicit**. In the `unlocked-version` observation version 1.5.0 of the package was released, no file was edited in your repository, the exit code stayed 0, and the output went from `2026-10-15` to `15.10.2026`.
- Its counterpart `locked` is **in place**: `lock.json` held the version at `1.4.0`, and the output did not change.
- The bounding measurement `major-version` is **in place**: the `^1.4` constraint left 2.0.0 out. The constraint guards against the major version but not against the behavior of a minor version.
- The `distinction` and `flow` measurements showed two distinctions: `~1.4.0` stops at a patch version and `^1.4.0` at a minor version; when the lock file is in the repository, everyone on the team installs the same version, and an update knowingly rewrites the lock.
- The manifest file records what you ask for and the lock file records what you got; a version constraint is a range, and semantic versioning is a promise.

#### Next Step

The package manager gathered in one place which version of a dependency is loaded. But who builds the dependencies of a class? When a dependency changes, which file is edited? The Dependency Injection lesson measures this.

### Dependency Injection

Source: https://academia.sh/en/courses/php-object-model/dependency-injection

Constructor injection, depending on an interface, and manual wiring; a service's dependency changing only in the setup file; building the dependency inside moving the change onto the services; the seam holding only as long as the contract is kept; the measurement table of twelve lessons.

The Dependency Manager lesson gathered which version of the package the code depends on into a single lock file. Who builds the dependency of a class, though? The question in this lesson is this: if an object a service needs changes, **where does this change reach**, and which file is edited? M10/K04 asked **what a component needs**; what is measured here is whether the change reaches the consumers when that need changes.

#### Who Builds the Dependency

The object a service needs is called a **dependency**. The service obtains its dependency in one of two ways: it either builds it inside itself with `new`, or receives it from outside already built. The second way is called **dependency injection**, and in PHP one form of it is **constructor injection**: the object asks for its dependency as a parameter of the constructor.

The second decision is the type of the parameter. If the service writes the name of an **interface** rather than the name of a concrete class, every class that implements that interface is accepted. The service no longer knows which class its dependency is, only which contract it satisfies. The work of bringing objects together is called **wiring**, and when done by hand it is gathered in a single place, usually a setup file; this file is **the seam**.

**SP5.** The lesson measures only constructor injection. Injection through a property or a method, an IoC container resolving dependencies on its own, and whether objects are shared are not modeled. The wiring in the measurement is an array of functions.

#### 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 it. It asks 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 `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;
}
```

#### Manual Wiring: At the Seam

The observation is `manual-wiring`: the `Profile` service asks for a `Store`, writes a name, and reads it back. The base has three files. `store.php` carries the interface and the `ArrayStore` implementation. In `main.php`, the `Profile` constructor asks for a parameter of type `Store` and does not know which store it will get. `setup.php` returns an array of functions: when the `store` key is called, it builds an `ArrayStore`.

That array is **the smallest IoC container**. You give a name and get a built object; how the object is built is written only in this file. The small block below shows that each call builds a new instance, and that the array gives the class name of the implementation only here.

```php
<?php
// container.php — an array of functions is the smallest container
interface Store
{
    public function read(string $key): ?string;
}
class ArrayStore implements Store
{
    public function read(string $key): ?string { return null; }
}
$setup = ['store' => fn () => new ArrayStore()];
$a = $setup['store']();
$b = $setup['store']();
echo get_class($a), "\n";
echo $a === $b ? 'same instance' : 'separate instance', "\n";
```

```
ArrayStore
separate instance
```

The change is in a single file: `setup.php` now wraps the store in a `CountingStore` that counts writes. `Profile` and `main.php` stay where they are. The next block writes the base to disk as `base.php`; the later drivers pull it in. The fourth argument is the seam list and names `setup.php` as the seam.

```php
<?php
// base.php — Profile, Store, and the setup file
return [
    'store.php' => <<<'PHP'
        <?php
        interface Store
        {
            public function write(string $key, string $value): void;
            public function read(string $key): ?string;
        }
        class ArrayStore implements Store
        {
            private array $data = [];
            public function write(string $key, string $value): void { $this->data[$key] = $value; }
            public function read(string $key): ?string { return $this->data[$key] ?? null; }
        }
        PHP,
    'setup.php' => <<<'PHP'
        <?php
        return ['store' => fn () => new ArrayStore()];
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'store.php';
        class Profile
        {
            public function __construct(private Store $store) {}
            public function setName(string $name): string { $this->store->write('name', $name); return $this->store->read('name'); }
        }
        $setup = require 'setup.php';
        echo (new Profile($setup['store']()))->setName('eve'), "\n";
        PHP,
];
```

```php
<?php
// wiring.php — the store is wrapped in the setup file
require_once 'reach.php';

measure('manual-wiring', require 'base.php', [
    'setup.php' => <<<'PHP'
        <?php
        class CountingStore implements Store
        {
            public int $writes = 0;
            public function __construct(private Store $inner) {}
            public function write(string $key, string $value): void { $this->writes++; $this->inner->write($key, $value); }
            public function read(string $key): ?string { return $this->inner->read($key); }
        }
        return ['store' => fn () => new CountingStore(new ArrayStore())];
        PHP,
], ['setup.php']);

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

```
manual-wiring      files=1  exit=0   output same    -> at-seam
eve -> eve
```

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 second line is the output of the two versions: `eve` before and after. `Profile` did not know it had received a new dependency, because it recognized only the `Store` type. The counting feature, that is, a new behavior added to the service, arrived without `Profile` being edited.

#### The Counterpart: Building the Dependency Inside

If the same services had built their dependencies in their own constructors with `new ArrayStore()`, the difference would not be visible at first glance. The difference shows up in the change. In the base below, two services named `Profile` and `Cart` build their stores themselves, and the constructor of `ArrayStore` has no parameters. The change is in one file and touches only `ArrayStore`: the constructor asks for a key prefix.

```php
<?php
// inner_new.php — two services build their dependency inside themselves
require_once 'reach.php';

measure('inner-new', [
    'store.php' => <<<'PHP'
        <?php
        class ArrayStore
        {
            private array $data = [];
            public function write(string $key, string $value): void { $this->data[$key] = $value; }
            public function read(string $key): ?string { return $this->data[$key] ?? null; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'store.php';
        class Profile
        {
            private ArrayStore $store;
            public function __construct() { $this->store = new ArrayStore(); }
            public function setName(string $name): string { $this->store->write('name', $name); return $this->store->read('name'); }
        }
        class Cart
        {
            private ArrayStore $store;
            public function __construct() { $this->store = new ArrayStore(); }
            public function setItem(string $item): string { $this->store->write('item', $item); return $this->store->read('item'); }
        }
        echo (new Profile())->setName('eve'), ' ', (new Cart())->setItem('pen'), "\n";
        PHP,
], [
    'store.php' => <<<'PHP'
        <?php
        class ArrayStore
        {
            private array $data = [];
            public function __construct(private string $prefix) {}
            public function write(string $key, string $value): void { $this->data[$this->prefix . $key] = $value; }
            public function read(string $key): ?string { return $this->data[$this->prefix . $key] ?? null; }
        }
        PHP,
]);

echo run('measure/inner-new/before')[1], "\n";
```

```
inner-new          files=1  exit=255 output changed -> breaking
eve pen
```

The reach is **breaking**: one file changed, and the exit code was 255. The last line is the output of the earlier version; the later version could print nothing. `Profile` and `Cart` were not edited, but the `new ArrayStore()` call in the constructor could not supply the new required parameter, and the program stopped. Because the same pattern repeats in every service, the change in the dependency reached both of these services. In the first case the place that built the dependency was a single file; here the place that builds it is the body of each service, so the number of seams is the number of services.

**SP6.** In the counterpart measurement, the breaking result comes from the required parameter added to the `ArrayStore` constructor. If the parameter had been optional, or the constructor had not changed at all, the measurement would have given another result; the lesson does not measure this.

#### The Bounding Measurement

The seam absorbed the change, but it does not absorb every change. The seam's validity depends on the contract: the `Profile` constructor asks for the `Store` type. In this measurement, `setup.php` returns an object that carries the `write` and `read` methods but does not implement `Store`. The method names are the same; only `implements Store` is missing. The base is the `manual-wiring` base, and the change is again only in `setup.php`.

```php
<?php
// no_contract.php — setup returns an object that does not implement Store
require_once 'reach.php';

measure('no-contract', require 'base.php', [
    'setup.php' => <<<'PHP'
        <?php
        class DictionaryStore
        {
            private array $data = [];
            public function write(string $key, string $value): void { $this->data[$key] = $value; }
            public function read(string $key): ?string { return $this->data[$key] ?? null; }
        }
        return ['store' => fn () => new DictionaryStore()];
        PHP,
], ['setup.php']);
```

```
no-contract        files=1  exit=255 output changed -> breaking
```

The same file, the same seam list, but the reach became **breaking**: the exit code is 255. The changed file was in the seam list; this was not enough, because in the order the exit code comes before the seam. The type declaration in the `Profile` constructor turned `DictionaryStore` away. No message was printed in this measurement; the mechanism that stopped it is the same type check as in the Type Declarations lesson. Although `DictionaryStore` has the right methods, it did not declare the contract. The seam keeps the change in a single file only as long as the contract is kept; a wiring that breaks the contract stops the consumer.

**SP7.** The `no-contract` measurement models the case where the wiring code is faulty: there is a type declaration, and the one doing the wiring supplies the wrong object. Without the type declaration, `Profile` could have worked with the same method names; the lesson does not measure this case.

#### Summary

- Dependency injection means a service receives the object it needs from its constructor instead of building it inside with `new`; an interface type separates the service from the implementation.
- The reach of this lesson is **at the seam**. In the `manual-wiring` observation the store was wrapped in `setup.php`, one file changed, the exit code stayed 0, and the output stayed `eve`. An array of functions is the smallest container.
- Its counterpart `inner-new` is **breaking**: because two services build the store inside themselves, adding a parameter to the `ArrayStore` constructor stopped both, with exit code 255.
- The bounding measurement `no-contract` is **breaking**: when `setup.php` returned an object that does not implement `Store`, the type declaration in the `Profile` constructor stopped it, with exit code 255.
- The seam keeps the change in a single file only as long as the contract is kept.

#### Course Wrap-Up

The course asked a single question twelve times: **where does this change reach?** Each lesson took a single change from the core and placed it in one of four reaches. The table below takes the reach of each lesson from its `## Summary` section, and its bounding measurement from the lesson's `## The Bounding Measurement` section.

| Lesson | Change measured | Reach | Bounding measurement |
|---|---|---|---|
| Classes and Objects | name of a private property | in place | consumer writing to a public property: in place, `E_DEPRECATED` |
| Inheritance and Abstract Classes | parent class dropped an internal call | implicit | counter by composition: in place, `3` |
| Interfaces | method added to the interface | breaking | abstract class with a body: in place, `120` |
| Traits | conflicting name added to the trait | breaking | the class's own method: in place |
| Static Members and Constants | constructor signature, static factory | in place | constant value `20` → `18`: implicit, `120` → `118` |
| Magic Methods | name of the `__get` key | implicit | exception on an unknown name: breaking |
| Type Declarations | `int` added to a parameter | breaking | `?int` and `'5'`: in place, `float(0)` and `float(6)` |
| Namespaces | outside code added a class of the same name | in place | unqualified `strlen`: implicit, `5` → `0` |
| Autoloading | class file moved, 3 files | at the seam | `product.php` in lowercase: breaking |
| Common Interface Standards | implementation changed | at the seam | `{number}` not filled: implicit |
| The Dependency Manager | minor version released, unlocked | implicit | `2.0.0` released: in place |
| Dependency Injection | store wrapped in setup | at the seam | an object that is not a `Store`: breaking |

The distribution of the lessons is **3 in place · 3 at the seam · 3 breaking · 3 implicit**. Because each lesson carried a single observation from the core, this distribution is the same as the distribution of the twelve changes of the core; the core itself was not run and printed in any lesson, and the distribution was counted from the reach cells of the lessons.

Two observations from the table are the thesis of the course. The first is that the reach comes not from the size of the change but from **what the consumer touches**: the same definition change left the consumer that reads implicit and the consumer that writes in place; the same method addition was breaking when it had no body and in place when it had one. The second is that the most dangerous reach is **implicit**: a breaking change gives itself away at the first run, while an implicit change is seen only when the result is examined. The bounding measurements showed in every lesson that the reach depends on a condition; the four reaches are the measure not of a definition but of the tie between the definition and the consumer.

The next course is M12/K03, PHP on the Web: Sessions, Data and Security. That course covers how sessions and cookies carry state across requests, database access and the parameterized query, and then web security. The distinction this course brought, the four names that classify where a change reaches, will be useful there too when reading how a change affects the consumer.

