Skip to content

Commit 8f573b3

Browse files
authored
Merge pull request #14707 from nextcloud/backport/13840/stable33
2 parents 815a4a8 + cb3723e commit 8f573b3

1 file changed

Lines changed: 122 additions & 30 deletions

File tree

developer_manual/basics/dependency_injection.rst

Lines changed: 122 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,162 @@
1-
====================
2-
Dependency injection
3-
====================
4-
5-
.. sectionauthor:: Bernhard Posselt <dev@bernhard-posselt.com>
6-
7-
The App Framework assembles the application by using a container based on the
8-
software pattern `Dependency Injection <https://en.wikipedia.org/wiki/Dependency_injection>`_.
9-
This makes the code easier to test and thus easier to maintain.
10-
11-
If you are unfamiliar with this pattern, watch the following video:
1+
=================================
2+
Containers / Dependency Injection
3+
=================================
4+
5+
Introduction
6+
------------
7+
8+
Modern software applications are composed of various components that need to interact
9+
with one another. Traditionally, objects create their own dependencies internally,
10+
which leads to tight coupling and makes code harder to test, maintain, and extend.
11+
`Dependency injection (DI) <https://en.wikipedia.org/wiki/Dependency_injection>`_ is
12+
a software design pattern that helps solve this problem by having dependencies provided
13+
from the outside, rather than being constructed inside the object itself.
14+
15+
Dependency injection may sound like a big concept, but it’s really just about making
16+
your code easier to work with and more flexible. Instead of each part of your app
17+
creating the things it needs by itself, those “dependencies” are handed to it -- usually
18+
by a special helper called a container. This means your classes don’t need to know how
19+
to create their collaborators; they just need to know how to use them.
20+
21+
The App Framework in Nextcloud assembles applications using a container based on this
22+
design pattern. This approach leads to more modular, testable, and maintainable code.
23+
24+
Using dependency injection is about more than just elegant code. When all apps follow
25+
this pattern:
26+
27+
- It’s easier to test and upgrade both apps and the server, since dependencies can be
28+
swapped out or mocked.
29+
- Apps stay decoupled from internal server details, making it safer for Nextcloud to
30+
evolve without breaking your app.
31+
- Core features like autowiring, service discovery, and new APIs become available to all
32+
apps without extra boilerplate.
33+
- Memory and resource usage can be reduced.
34+
- New services or APIs become easier to adopt as Nextcloud evolves.
35+
36+
By sharing a consistent approach to building and wiring up dependencies, everyone --
37+
core and app developers alike -- benefits from a more robust, secure, and future-proof
38+
platform.
39+
40+
If you are unfamiliar with the DI design pattern, don't worry -- it's widely used in
41+
modern frameworks, and you'll soon become comfortable with it. You can also watch the
42+
following video introduction:
1243

1344
* `Google Clean Code Talks <https://www.youtube.com/watch?v=RlfLCWKxHJ0>`_
1445

1546
.. _dependency-injection:
1647

17-
Dependency injection
18-
--------------------
48+
Basic Pattern of Dependency Injection
49+
-------------------------------------
50+
51+
The essence of dependency injection is: **don't instantiate dependencies directly inside
52+
your classes or methods, but instead pass them in as parameters**. This allows swapping
53+
out dependencies (such as with mocks in unit tests), makes dependencies explicit, and
54+
centralizes object creation logic.
1955

20-
Dependency Injection sounds pretty complicated but it just means: Don't put
21-
new dependencies in your constructor or methods but pass them in. So this:
56+
For example, consider the following pattern:
2257

2358
.. code-block:: php
59+
:emphasize-lines: 10, 14
60+
61+
/**
62+
* Without dependency injection:
63+
*/
2464
2565
use OCP\IDBConnection;
2666
27-
// without dependency injection
2867
class AuthorMapper {
68+
69+
// Define a property to store the dependency
2970
private IDBConnection $db;
3071
3172
public function __construct() {
73+
// The dependency is instantiated within the class
3274
$this->db = new Db();
3375
}
3476
}
3577
36-
would turn into this by using Dependency Injection:
78+
With dependency injection, you would instead request the dependency as part of the constructor parameters:
3779

3880
.. code-block:: php
81+
:emphasize-lines: 10, 13, 15
82+
83+
/**
84+
* Using dependency injection:
85+
*/
3986
4087
use OCP\IDBConnection;
4188
42-
// with dependency injection
4389
class AuthorMapper {
90+
91+
// Define a property to store the dependency
4492
private IDBConnection $db;
4593
94+
// The dependency is passed in from outside (typically by the container)
4695
public function __construct(IDBConnection $db) {
96+
// Assigned to the property
4797
$this->db = $db;
4898
}
4999
}
50100
51-
Controller injection
101+
Or, more succinctly, by using constructor property promotion (available in current PHP versions).
102+
The following is exactly equivalent:
103+
104+
.. code-block:: php
105+
:emphasize-lines: 18
106+
107+
/**
108+
* Using dependency injection with constructor property promotion:
109+
*/
110+
111+
use OCP\IDBConnection;
112+
113+
class AuthorMapper {
114+
115+
/**
116+
* Constructor property promotion with DI reduces boilerplate code by
117+
* handling everything within the constructor parameters. The example below
118+
* does exactly the same thing as the prior example, but in less code:
119+
*
120+
* - The dependency is passed in from outside (by the container)
121+
* - The private property is established to store the dependency
122+
* - The dependency is assigned directly to that property
123+
*/
124+
public function __construct(private IDBConnection $db) {
125+
}
126+
}
127+
128+
Advantages
129+
----------
130+
131+
- **Testability:** You can inject mock objects for unit testing.
132+
- **Maintainability:** Changing how a dependency is constructed (in the container)
133+
updates it wherever it is injected in the application.
134+
- **Explicitness:** Dependencies are clearly listed in constructors or method signatures,
135+
improving readability and maintainability.
136+
137+
Controller Injection
52138
--------------------
53139

54-
For controllers it's possible to also have dependencies injected into methods.
140+
For controllers, Nextcloud allows dependencies to also be injected directly into
141+
individual methods, not just constructors. This is referred to as *method injection* and
142+
enables you to specify dependencies only where needed, potentially reducing resource
143+
usage for rarely required services.
55144

56145
.. code-block:: php
57146
:caption: lib/Controller/ApiController.php
58-
:emphasize-lines: 12-13, 16-17
147+
:emphasize-lines: 15-16, 19-20
59148
60149
<?php
61150
62151
namespace OCA\MyApp\Controller;
63152
153+
use OCA\MyApp\Service\BarService;
154+
use OCA\MyApp\Service\FooService;
155+
use OCP\AppFramework\Controller;
64156
use OCP\IRequest;
65157
66-
class ApiController {
67-
public function __construct($appName, IRequest $request) {
158+
class ApiController extends Controller {
159+
public function __construct(string $appName, IRequest $request) {
68160
parent::__construct($appName, $request);
69161
}
70162
@@ -110,7 +202,7 @@ use the **IRegistrationContext::registerService** method:
110202
use OCP\AppFramework\Bootstrap\IBootContext;
111203
use OCP\AppFramework\Bootstrap\IRegistrationContext;
112204
use OCP\IDBConnection;
113-
205+
use OCP\IRequest;
114206
use OCA\MyApp\Controller\AuthorController;
115207
use OCA\MyApp\Service\AuthorService;
116208
use OCA\MyApp\Db\AuthorMapper;
@@ -136,7 +228,7 @@ use the **IRegistrationContext::registerService** method:
136228
$context->registerService(AuthorController::class, function(ContainerInterface $c): AuthorController {
137229
return new AuthorController(
138230
$c->get('appName'),
139-
$c->get(Request::class),
231+
$c->get(IRequest::class),
140232
$c->get(AuthorService::class)
141233
);
142234
});
@@ -171,13 +263,13 @@ The container works in the following way:
171263

172264
return new AuthorController(
173265
$c->get('appName'),
174-
$c->get(Request::class),
266+
$c->get(IRequest::class),
175267
$c->get(AuthorService::class)
176268
);
177269

178270
* The **appName** is queried and returned from the base class
179271
* The **Request** is queried and returned from the server container
180-
* **AuthorService** is queried::
272+
* **AuthorService** is queried. This triggers the registered Callable::
181273

182274
$container->registerService(AuthorService::class, function(ContainerInterface $c): AuthorService {
183275
return new AuthorService(
@@ -188,14 +280,14 @@ The container works in the following way:
188280
* **AuthorMapper** is queried::
189281

190282
$container->registerService(AuthorMapper::class, function(ContainerInterface $c): AuthorMapper {
191-
return new AuthorService(
283+
return new AuthorMapper(
192284
$c->get(IDBConnection::class)
193285
);
194286
});
195287

196288
* The **database connection** is returned from the server container
197-
* Now **AuthorMapper** has all of its dependencies and the object is returned
198-
* **AuthorService** gets the **AuthorMapper** and returns the object
289+
* Now **AuthorMapper** has all of its dependencies and can be constructed by the DI code. The object is returned.
290+
* **AuthorService** gets the **AuthorMapper**, is constructed, and returns the object
199291
* **AuthorController** gets the **AuthorService** and finally the controller can be instantiated and the object is returned
200292

201293
So basically the container is used as a giant factory to build all the classes that are needed for the application. Because it centralizes all the creation of objects (the **new Class()** lines), it is very easy to add new constructor parameters without breaking existing code: only the **__construct** method and the container line where the **new** is being called need to be changed.

0 commit comments

Comments
 (0)