---
title: 'Dependency Injection'
source: 'https://academia.sh/en/courses/php-object-model/dependency-injection'
course: 'Object-Oriented PHP and Standards'
language: en
updated: '2026-10-21T05:03:35+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that the dependency of a service built with constructor injection changes only in the setup file'
    - 'Compares a service that builds its dependency inside, which stops when the constructor of the dependency changes'
    - 'Shows that the seam holds the binding only as long as the contract is kept'
    - 'Summarizes the twelve changes measured across the course by four reaches'
---

# 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.
