Skip to content

Commit 33e9130

Browse files
Merge pull request #1 from pascalheidmann/di
DI
2 parents 444a446 + b1062d3 commit 33e9130

File tree

1 file changed

+301
-0
lines changed

1 file changed

+301
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
---
2+
title: "Inside Dependency Injection: building DI from scratch"
3+
date: 2024-03-015T15:34:56+02:00
4+
draft: false
5+
tags: [ "dependency injection", "PHP", "software design" ]
6+
description: "Most frameworks nowadays use Dependency Injection. In this article I will get inside the basics how a DI works by building one from scratch"
7+
---
8+
9+
Most frameworks nowadays use dependency injection.
10+
In this article, I will get inside the basics how a DI works by building one in PHP from scratch.
11+
12+
## What is a Dependency Injection (DI)
13+
14+
A Dependency Injection system is used to solve the issue of having to
15+
(re-)using instances of services around the application by providing a central way to access each and every service.
16+
They also take care of making all required dependencies available just in time — therefore the name.
17+
18+
## Road to service container
19+
20+
A typical service you will need again and again in an application is your database connection.
21+
You want to configure and establish it once and then be able to reuse it all the time.
22+
Therefore this will be my example service for the following examples
23+
24+
### Globals
25+
26+
Most programming languages have different scopes for their variables,
27+
with most languages having a concept of a global variable.
28+
Depending on the language you have to either define it explicitly as global
29+
(for example in PHP) or it will run implicitly available in sub contexts (like in JS).
30+
31+
```php
32+
global $DB = new DatabaseConnection('localhost', 3306, 'user', 'password');
33+
34+
function myFunction() {
35+
$result = $DB->query('SELECT * FROM foo');
36+
}
37+
```
38+
39+
There are several obvious issues with this approach:
40+
41+
1. As this is a variable, I can override it at any time in any part of my application
42+
breaking havoc if my `$DB` now not the same database connection anymore.
43+
Maybe two devs where developing their features and both needed some database and used the same variable.
44+
It worked as long as they did not merge, but now you cannot tell which connection is behind that descriptive
45+
variable.
46+
47+
2. It is global by its nature, and every part of the application can use it.
48+
I cannot trace it uses through the application because one part of the application might define its own **local
49+
** `$DB`.
50+
51+
3. I always have to instantiate it even if I don't need it. This is a waste of resources.
52+
53+
### Singletons
54+
55+
Singletons are the result of the mentioned issues.
56+
They are a software pattern
57+
that aims to have a single instance of a given object that will be reused for every consecutive use case.
58+
To achieve this task there is always a method to get the same instance again, and if none exists yet they will create
59+
it.
60+
61+
Pure function:
62+
63+
```php
64+
function getDbInstance(): DatabaseConnection {
65+
static $instance;
66+
67+
if (!$instance) {
68+
$instance = new DatabaseConnection('localhost', 3306, 'user', 'password');
69+
}
70+
71+
return $instance;
72+
}
73+
74+
$db = getDbInstance();
75+
```
76+
77+
OOP
78+
79+
```php
80+
class DatabaseConnection {
81+
private static $instance = null;
82+
83+
// explicit constructor to prevent `new DatabaseConnection()`
84+
protected function __construct() {}
85+
86+
public static function getInstance(): self {
87+
if (!self::$instance) {
88+
self::$instance = new self('localhost', 3306, 'user', 'password');
89+
}
90+
return self::$instance;
91+
}
92+
}
93+
94+
$db = DatabaseConnection::getInstance();
95+
```
96+
97+
As classes and functions are (in most languages) immutable,
98+
you still have them in global space, but you can be sure that they will return the same object every time.
99+
On the flip side:
100+
you have to explicitly make a class or service a singleton or provide a singleton wrapper, which means extra work.
101+
Also,
102+
you now have either the configuration global instead or weave it
103+
(like in my example) directly into your service, which makes it harder to reuse.
104+
105+
Something we haven't talked about yet is testing.
106+
Considering the example with our database connection,
107+
we normally do not want to call our production database
108+
and therefore need a different connection with different configuration.
109+
In this case, the aforementioned configuration within our service is a big no-go.
110+
111+
## Containers
112+
113+
Containers are a standardized way to store things.
114+
A standard shipment container is typically 10, 20 or 40 ft long, with a width of 8ft and height of 8.5ft.
115+
They have doors and can be stacked however the operator likes.
116+
What's inside does not matter — may it be cheap chinese fast fashion, bananas or cocaine (or bananas and cocaine).
117+
118+
In the programming world we also use lots of containers for storing things. One of the kind is the _service container_.
119+
120+
```php
121+
class ServiceContainer {
122+
private static $instance;
123+
private array $services = [];
124+
125+
public static function getInstance(): self {
126+
if (!self::$instance) {
127+
self::$instance = new self('localhost', 3306, 'user', 'password');
128+
}
129+
return self::$instance;
130+
}
131+
132+
public function set(string $name, mixed $service) {
133+
$this->services[$name] = $service;
134+
}
135+
136+
public function get(string $name) {
137+
if (!isset($this->services[$name])) {
138+
throw new RuntimeException('Unknown service ' . $name);
139+
}
140+
return $this->services[$name];
141+
}
142+
}
143+
144+
// ...
145+
146+
// For now: use singleton to make service container available everywhere
147+
// configuration
148+
$container = ServiceContainer::getInstance();
149+
$container->set('MyVeryCoolSecreteDatabaseConnection', new DatabaseConnection('localhost', 42000, 'user', 'password'));
150+
$container->set(DatabaseConnection::class, new DatabaseConnection('127.0.0.1', 3306, 'user', 'password'));
151+
152+
// ...
153+
// In my app now I can do following
154+
$db = ServiceContainer::getInstance()->get('MyVeryCoolSecreteDatabaseConnection');
155+
$result = $qb->query('SELECT * FROM foo');
156+
157+
158+
$db2 = ServiceContainer::getInstance()->get(DatabaseConnection::class);
159+
$result = $qb2->query('SELECT * FROM foo');
160+
```
161+
162+
So far, so good.
163+
To make things easy it is common practice to use the fully qualified class name
164+
(FQCN)
165+
of the service you want to share although no-one will prevent you to provide a service under whatever name you like.
166+
Hence, you can even have several instances of a service like different database connections
167+
using the same `DatabaseConnection` class.
168+
169+
When testing, we sometimes we do not want to have a connection to a database at all
170+
but instead use a mocked service that always returns the same, predefined values.
171+
What do we now?
172+
Well, thanks to `interfaces` we can now do the following:
173+
174+
```php
175+
interface DatabaseConnectionInterface {
176+
public function query(string $sql): array;
177+
}
178+
179+
class DatabaseConnection implements DatabaseConnectionInterface {}
180+
class MockDatabaseConnection implements DatabaseConnectionInterface {}
181+
182+
// CONFIGURATION
183+
// In productionx
184+
$container->set(DatabaseConnectionInterface::class, new DatabaseConnection('127.0.0.1', 3306, 'user', 'password'));
185+
186+
// For testing
187+
$container->set(DatabaseConnectionInterface::class, new MockDatabaseConnection());
188+
189+
// ...
190+
// In our application: we use whatever implementation is available
191+
$db = ServiceContainer::getInstance()->get(DatabaseConnectionInterface::class);
192+
$result = $qb->query('SELECT * FROM foo');
193+
```
194+
195+
## Factories
196+
197+
In software design we use the [factory pattern](https://refactoring.guru/design-patterns/factory-method)
198+
to create new instances of a specific component.
199+
This can be a `WindowFactory`
200+
that creates a new `Window` in your UI with specific configurations or like in our example a new database connection.
201+
202+
If you paid a little bit attention one goal of the `Singleton` pattern was the instantiation when it is first needed
203+
(`getInstance()`) which we lost in our previous example with our service container.
204+
But fear not: there is an easy fix by extending out
205+
206+
```php
207+
class ServiceContainer {
208+
private array $services = [];
209+
210+
// this is new
211+
private array $factories = []:
212+
213+
public function set(string $name, mixed $service) {
214+
$this->services[$name] = $service;
215+
}
216+
217+
public function setFactory(string $name, callable $factory) {
218+
$this->factories[$name] = $factory;
219+
}
220+
221+
public function get(string $name) {
222+
if (!isset($this->services[$name])) {
223+
// this is new
224+
if (!isset($this->factories[$name])) {
225+
throw new RuntimeException('Unknown service ' . $name);
226+
}
227+
$this->set($name, $factory($this, $name));
228+
}
229+
return $this->services[$name];
230+
}
231+
}
232+
```
233+
234+
With this new extended `ServiceContainer` we have two ways to get a service:
235+
it is either given already preconfigured or we have a factory-callback that will return the required instance.
236+
237+
```php
238+
$serviceContainer->setFactory(DatabaseConnectionInterface::class, fn() => new DatabaseConnection('127.0.0.1', 3306, 'user', 'password')));
239+
240+
// ...
241+
242+
// create instance only when needed
243+
$db = $serviceContainer->get(DatabaseConnectionInterface::class);
244+
$db->query(...);
245+
```
246+
247+
### Using factories recursive
248+
249+
So now we can configure our database connection in one place
250+
and can reuse it somewhere else in our application without having to access anything but our `ServiceContainer`.
251+
But we can still do better!
252+
Let's use our new factory functionality
253+
to create dependent services like a `DocomentRepository` that is fetching documents from our database.
254+
Maybe you have seen that we passed our `ServiceContainer` to our factory although its factory did not use it?
255+
256+
```php
257+
class DocumentRepository {
258+
public function __construct(private DatabaseConnectionInterface $db) {}
259+
260+
public function find($id): Document {
261+
return $this->db->query('SELECT * FROM document WHERE id=' . $id);
262+
}
263+
}
264+
265+
$serviceContainer->setFactory(DatabaseConnectionInterface::class, fn() => new DatabaseConnection('127.0.0.1', 3306, 'user', 'password')));
266+
$serviceContainer->setFactory(
267+
DocumentRepository::class,
268+
fn(ServiceContainer $container) => new DocumentRepository($container->get(DatabaseConnectionInterface::class))
269+
);
270+
271+
// ...
272+
273+
$repository = $serviceContainer->get(DocumentRepository::class);
274+
$repository->find(1);
275+
```
276+
277+
So what are we doing here?
278+
Thanks to our service definition we have two services defined via their factory methods,
279+
with the `DocumentRepository` being dependent upon the `DatabaseConnection`.
280+
When we first request our `DocumentRepository` its factory method will be called.
281+
This factory method itself now uses the `ServiceContainer`
282+
and triggers the creation of the `DatabaseConnection`
283+
to provide its instance to the constructor of the `DocumentRepository`.
284+
285+
## Conclusion
286+
287+
Thanks to our new dependency injection, we are now able to
288+
289+
- configure our services in a single space in our code
290+
- still be able to initiate our services lazily and only if needed
291+
- decouple services and their dependencies (as long as we use interfaces) which enhances our ability to mock without
292+
much effort a lot
293+
- leave our global variable scope clean as our only entry point is our `ServiceContainer` which encapsulates everything
294+
else — still while not knowing anything over any service it manages
295+
296+
297+
### So what's next?
298+
In a future blog post, I want to elaborate on the idea and introduce some advanced features like an `alias` system,
299+
structure for systematic creation & configuration
300+
as well as so called `autowiring`
301+
to reduce the work we have to put in to create new service instances that are only dependent on other services.

0 commit comments

Comments
 (0)