---
title: 'Object Container and Facades'
source: 'https://academia.sh/en/courses/web-application-framework/object-container-and-facades'
course: 'The Web Application Framework'
language: en
updated: '2026-10-21T05:03:39+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

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

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
<?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
<?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
<?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
<?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 `FileLogger` was not wired and `MemoryLogger` ran.
- In the container with a single binding the same `FileLogger` ran; 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: `FileLogger` ran, and `MemoryLogger` was 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?
