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-wiringobservation the store was wrapped insetup.php, one file changed, the exit code stayed 0, and the output stayedeve. An array of functions is the smallest container. - Its counterpart
inner-newis breaking: because two services build the store inside themselves, adding a parameter to theArrayStoreconstructor stopped both, with exit code 255. - The bounding measurement
no-contractis breaking: whensetup.phpreturned an object that does not implementStore, the type declaration in theProfileconstructor 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.