php
65 lines · 9 steps
Building a DI container with reflection in PHP
A dependency injection container that registers factories and auto-wires constructor dependencies using reflection.
Explained by
highlit
1final class Container
2{
3 private array $bindings = [];
4 private array $instances = [];
5
6 public function bind(string $abstract, callable $factory): void
7 {
8 $this->bindings[$abstract] = $factory;
9 }
10
11 public function singleton(string $abstract, callable $factory): void
12 {
13 $this->bindings[$abstract] = function (Container $c) use ($abstract, $factory) {
14 return $this->instances[$abstract] ??= $factory($c);
15 };
16 }
17
18 public function make(string $abstract): object
19 {
20 if (isset($this->bindings[$abstract])) {
21 return ($this->bindings[$abstract])($this);
22 }
23
24 return $this->resolve($abstract);
25 }
26
27 private function resolve(string $class): object
28 {
29 $reflector = new ReflectionClass($class);
30
31 if (!$reflector->isInstantiable()) {
32 throw new RuntimeException("Cannot instantiate {$class}");
33 }
34
35 $constructor = $reflector->getConstructor();
36
37 if ($constructor === null) {
38 return new $class();
39 }
40
41 $arguments = array_map(
42 fn (ReflectionParameter $param) => $this->resolveParameter($param, $class),
43 $constructor->getParameters()
44 );
45
46 return $reflector->newInstanceArgs($arguments);
47 }
48
49 private function resolveParameter(ReflectionParameter $param, string $class): mixed
50 {
51 $type = $param->getType();
52
53 if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
54 return $this->make($type->getName());
55 }
56
57 if ($param->isDefaultValueAvailable()) {
58 return $param->getDefaultValue();
59 }
60
61 throw new RuntimeException(
62 "Unresolvable dependency \${$param->getName()} in {$class}"
63 );
64 }
65}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A container decouples object creation from usage by mapping abstract names to factories or reflected classes.
- 2Reflection lets you inspect constructor signatures and recursively resolve typed dependencies without manual wiring.
- 3Caching resolved instances behind a closure turns any binding into a lazily-created singleton.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-di-container-with-reflection-in-php-explained-php-43a3/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.