Lesson 06 / 21
Object Container and Facades
Binding in the object container, singleton binding and autowiring, a second binding to the same name silently overwriting the first, and the facade resolving the object from the container on every call.
Published Updated
In this lesson
- Measures that a second binding to the same name overwrites the first without an error, and that the overwritten implementation falls under not wired
- Compares that in a container with a single binding the same implementation falls under ran
- Measures that the facade resolves the object from the container on every call, and that singleton binding hides rebinding
Contents
The previous lesson showed that when one of the layers in front of the action gives a response, the rest of the chain is skipped. The layers, the controllers and their dependencies are built somewhere outside the chain. In a framework this place is the object container: a registry that builds the object requested by name and, if the object needs a dependency, builds that too and hands it over. Which binding the container chooses when a name is bound twice is what this lesson measures.
The Object Container Builds Objects in Three Ways
The object container has appeared twice before in this course; here we open all three of its ways. The first way is binding: bind() associates a name with a recipe, that is, a function that builds the object. The second way is singleton binding: the recipe runs once at the first resolution and the resulting object is stored; all later get() calls return the same object. The third way is autowiring: if the name is not bound, the container looks at the types of the constructor parameters of the class with ReflectionClass and takes each of them from the container in turn.
Why a dependency is supplied from outside was explained in the Dependency Injection lesson of M12/K02: the class does not build its dependency itself, it receives it from outside. There the one who supplied the dependency by hand was us. Here it is the container, and the price of this is that who builds what is moved into a registry. In M08/K06, the other face of the framework course, this handed-over control is called inversion of control, and the question there is from which source a behavior comes. The question here is a different one: whether a bound part actually ran in this request.
container.php builds the container. The Logger interface has two implementations: FileLogger and MemoryLogger. When write() is called, both report their own name to the ledger as ran. bind() writes each binding into the $recipes array by name; because the array key is the name, if the same name exists the earlier binding is silently overwritten. bindingsToPath() puts the bound parts in the container on the path: the path is the set of bindings the container holds in this request. Facade and Log will be used later in the lesson: a facade is a way of reaching an object in the container through a static name.
The order of the three ways is also visible inside get(), and the order matters. First the stored singleton object is looked at, then the bound recipe, and last autowiring. So if a singleton object exists for a name, the binding is never asked; if a binding exists, the constructor of the class is never read. This is why autowiring is the default behavior: every class that is not bound works, as long as the types of its constructor parameters can be resolved. If a parameter is an interface or a scalar value, resolution stops, because the container cannot know which implementation that name should go to. Tying an interface to an implementation is the real job of binding; for this reason the Logger interface cannot be resolved without a binding.
<?php // container.php — binding, singleton binding, autowiring and the facade declare(strict_types=1); require_once 'ran.php'; interface Logger { public function write(string $message): void; } final class FileLogger implements Logger { public function write(string $message): void { Ledger::ran('FileLogger'); } } final class MemoryLogger implements Logger { public function write(string $message): void { Ledger::ran('MemoryLogger'); } } final class ObjectContainer { private array $recipes = []; private array $singletons = []; public function bind(string $name, Closure $recipe, bool $singleton = false, string $part = ''): void { $this->recipes[$name] = [$recipe, $singleton, $part]; // if the name exists, it is silently overwritten } public function bindingsToPath(): void { foreach ($this->recipes as [, , $part]) Ledger::onPath($part); } public function get(string $name): object { if (isset($this->singletons[$name])) return $this->singletons[$name]; if (isset($this->recipes[$name])) { [$recipe, $singleton] = $this->recipes[$name]; $object = $recipe($this); if ($singleton) $this->singletons[$name] = $object; return $object; } $dependencies = []; foreach ((new ReflectionClass($name))->getConstructor()?->getParameters() ?? [] as $p) { $dependencies[] = $this->get($p->getType()->getName()); } return new $name(...$dependencies); } } abstract class Facade { private static ObjectContainer $container; public static function setContainer(ObjectContainer $container): void { self::$container = $container; } abstract protected static function name(): string; public static function __callStatic(string $method, array $arguments): mixed { return self::$container->get(static::name())->$method(...$arguments); // resolved from the container on every call } } final class Log extends Facade { protected static function name(): string { return Logger::class; } }
The ran.php core is given again in every lesson.
<?php // ran.php — did an expected part run in this request? declare(strict_types=1); final class Ledger { public static array $path = []; // parts placed on the request's path public static array $ran = []; // parts called within the request public static array $deferred = []; // parts left outside the request public static function newRequest(): void { self::$path = self::$ran = self::$deferred = []; } public static function onPath(string $part): void { self::$path[] = $part; } public static function ran(string $part): void { self::$ran[] = $part; } public static function defer(string $part): void { self::$deferred[] = $part; } } function classify(string $part): string { return match (true) { in_array($part, Ledger::$ran, true) => 'ran', in_array($part, Ledger::$deferred, true) => 'deferred', in_array($part, Ledger::$path, true) => 'skipped', default => 'not wired', }; }
FF18. If there is a single binding under the name Logger in the container, get() runs the recipe of that binding, and the write() call of the returned object appears as ran. If a second binding is added under the same name, the first binding in the $recipes array is deleted; resolution runs the second binding. The implementation of the first binding never enters the path, because only the bindings that remain in the container are put on the path.
The Second Binding Overwrites the First
Two containers are built. In the first, FileLogger is bound under the name Logger first and MemoryLogger after it. In the second, only FileLogger is bound. In each container the same request is made: the bindings are put on the path, Logger is taken from the container and write() is called.
<?php // binding.php — a second binding to the same name silently overwrites the first declare(strict_types=1); require_once 'container.php'; function measure(ObjectContainer $container): void { Ledger::newRequest(); $container->bindingsToPath(); $container->get(Logger::class)->write('recorded'); } $two = new ObjectContainer(); $two->bind(Logger::class, fn() => new FileLogger(), part: 'FileLogger'); $two->bind(Logger::class, fn() => new MemoryLogger(), part: 'MemoryLogger'); $one = new ObjectContainer(); $one->bind(Logger::class, fn() => new FileLogger(), part: 'FileLogger'); foreach (['two-bindings' => $two, 'one-binding' => $one] as $name => $container) { measure($container); foreach (['FileLogger', 'MemoryLogger'] as $part) { printf("%-13s %-13s %s\n", $name, $part, classify($part)); } }
two-bindings FileLogger not wired two-bindings MemoryLogger ran one-binding FileLogger ran one-binding MemoryLogger not wired
FF19. In the container with two bindings, MemoryLogger ran, but FileLogger was not wired: the line that binds it still stands in the file, and no error or warning appeared. In the container with a single binding, the same FileLogger ran. The difference is not in the presence of that binding in the file but in whether it remained in the container. The developer who wrote the binding may assume that the first line is in force; but what is in force in the container is the binding written last.
Why this class is hard to catch comes from the nature of binding. Bindings usually sit in the startup files of the framework and the application, in places far from each other: one is the framework’s own bindings, one is the application’s, one is a package’s. Which of two bindings that write to the same name will be last depends on the order in which the files are loaded. Each binding looks correct on its own; the conflict appears only when the two are loaded together. In the measurement we built this conflict with two consecutive lines; in a real application there may be hundreds of lines and several files between them.
The work the measurement draws from this is to ask for the expected implementation by name. Reading the binding is not enough, because the line that is read may not be the line in force. The list of parts on the path is the list of bindings that remain in the container; if the expected part is not in this list, the binding was either overwritten or never made, and the two look the same from outside.
This lesson’s outcome is not wired. How it differs from skipped is this: a skipped part was on the path and was not called because a layer in front gave a response; a part that is not wired never entered the path, because its binding is not in the container. A thing that leaves no trace in the ledger announces itself neither with an error message nor with a status code. For this reason not wired is the most dangerous outcome in the decision order, and to see it, the developer must have named the expected part.
The Bounding Measurement
Does a second binding overwriting the first hold in every condition? This measurement combines two things: singleton binding and the facade. When Log::write() is written, the Log facade runs PHP’s __callStatic magic method; this method resolves the name Logger from the container and passes the call to the object it obtains. The facade does not store the object; it takes it from the container again on every call.
The container is first bound to FileLogger as a singleton. After the first call, MemoryLogger is bound to the same name, and then the facade is called again.
<?php // facade.php — singleton binding hides rebinding declare(strict_types=1); require_once 'container.php'; $container = new ObjectContainer(); $container->bind(Logger::class, fn() => new FileLogger(), singleton: true, part: 'FileLogger'); Facade::setContainer($container); foreach (['first call', 'after rebinding'] as $step => $name) { if ($step === 1) { $container->bind(Logger::class, fn() => new MemoryLogger(), part: 'MemoryLogger'); } Ledger::newRequest(); $container->bindingsToPath(); Log::write('recorded'); printf("%s: File %s, Memory %s\n", $name, classify('FileLogger'), classify('MemoryLogger')); }
first call: File ran, Memory not wired after rebinding: File ran, Memory skipped
FF20. In the first call FileLogger ran, and by the rule of singleton binding the object was stored. The second binding really overwrote the recipe in $recipes: MemoryLogger entered the path. But before looking at the recipe, get() looks at the stored singleton object, and because it found it, it returned the old object. As a result, in the second call FileLogger ran again, and the rebound MemoryLogger was skipped although it was on the path. The result in the previous section, that is, the second binding winning, holds if the singleton has not yet been resolved; once it has been resolved, the stored object wins.
This limit has one more consequence: rebinding in a test or in the middle of a request together with singleton binding may not show the expected effect. After rebinding, the stored object must also be made to forget; otherwise the new binding looks as if it is in force, but resolution still gives the old one. The container used in this lesson does not do this, and this is left so on purpose, because for the limit to be visible the container must have a storing point.
This measurement also shows that the facade does not create a problem on its own. The facade asks the container on every call; what goes to the wrong object is not the facade but the container’s answer. The facade makes the problem harder to see because it hides this complexity behind a static name: the line Log::write() does not say which implementation will run.
Summary
- The object container builds objects in three ways: binding, singleton binding and autowiring; the facade is a way of reaching an object in the container through a static name.
- A second binding to the same name overwrites the first without an error or warning; in the container with two bindings
FileLoggerwas not wired andMemoryLoggerran. - In the container with a single binding the same
FileLoggerran; the difference is not that the binding is in the file but that it remains in the container. - After a singleton binding is resolved, rebinding does not change the stored object:
FileLoggerran, andMemoryLoggerwas on the path but was skipped. - This lesson’s outcome is not wired: a binding that does not remain in the container does not enter the path and leaves no trace in the ledger.
Next Step
For six lessons we built the parts of the framework one by one, by hand: the lifecycle, the route, the controller, the middleware, the container. The next topic, Views and Data, moves to the body of the response these produce, and its first lesson, Template Engine, asks this question: which step does the template run again each time, and if the result of that step is stored, what does the next request see?
To keep your progress and take notes, Log in
My notes
Log in to take notes.