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