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