Skip to content
academia.sh

Lesson 12 / 12

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.

Published Updated

In this lesson

  • 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
Contents

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

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close