Containers / Dependency Injection

Introduction

Modern software applications are composed of various components that need to interact with one another. Traditionally, objects create their own dependencies internally, which leads to tight coupling and makes code harder to test, maintain, and extend. Dependency injection (DI) is a software design pattern that helps solve this problem by having dependencies provided from the outside, rather than being constructed inside the object itself.

Dependency injection may sound like a big concept, but it’s really just about making your code easier to work with and more flexible. Instead of each part of your app creating the things it needs by itself, those “dependencies” are handed to it -- usually by a special helper called a container. This means your classes don’t need to know how to create their collaborators; they just need to know how to use them.

The App Framework in Nextcloud assembles applications using a container based on this design pattern. This approach leads to more modular, testable, and maintainable code.

Using dependency injection is about more than just elegant code. When all apps follow this pattern:

By sharing a consistent approach to building and wiring up dependencies, everyone -- core and app developers alike -- benefits from a more robust, secure, and future-proof platform.

If you are unfamiliar with the DI design pattern, don't worry -- it's widely used in modern frameworks, and you'll soon become comfortable with it. You can also watch the following video introduction:

Basic Pattern of Dependency Injection

The essence of dependency injection is: don't instantiate dependencies directly inside your classes or methods, but instead pass them in as parameters. This allows swapping out dependencies (such as with mocks in unit tests), makes dependencies explicit, and centralizes object creation logic.

For example, consider the following pattern:

With dependency injection, you would instead request the dependency as part of the constructor parameters:

Or, more succinctly, by using constructor property promotion (available in current PHP versions). The following is exactly equivalent:

Advantages

Controller Injection

For controllers, Nextcloud allows dependencies to also be injected directly into individual methods, not just constructors. This is referred to as method injection and enables you to specify dependencies only where needed, potentially reducing resource usage for rarely required services.

Using a container

Passing dependencies into the constructor rather than instantiating them in the constructor has the following drawback: Every line in the source code where new AuthorMapper is being used has to be changed, once a new constructor argument is being added to it.

The solution for this particular problem is to limit the new AuthorMapper to one file, the container. The container contains all the factories for creating these objects and is configured in :file:`lib/AppInfo/Application.php`.

Nextcloud 20 and later uses the :ref:`PSR-11 standard <psr11>` for the container interface, so working with the container might feel familiar if you've worked with other php applications before that also adhere to the convention.

To add the app's classes simply open the :file:`lib/AppInfo/Application.php` and use the IRegistrationContext::registerService method:

<?php

namespace OCA\MyApp\AppInfo;

use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\IDBConnection;
use OCP\IRequest;
use OCA\MyApp\Controller\AuthorController;
use OCA\MyApp\Service\AuthorService;
use OCA\MyApp\Db\AuthorMapper;
use Psr\Container\ContainerInterface;

class Application extends App implements IBootstrap {

  public function __construct(array $urlParams = []){
    parent::__construct('myapp', $urlParams);
  }

  public boot(IBootContext $context): void {
    // ...
  }

  /**
   * Define your dependencies in here
   */
  public function register(IRegistrationContext $context): void {
    /**
     * Controllers
     */
    $context->registerService(AuthorController::class, function(ContainerInterface $c): AuthorController {
      return new AuthorController(
        $c->get('appName'),
        $c->get(IRequest::class),
        $c->get(AuthorService::class)
      );
    });

    /**
     * Services
     */
    $context->registerService(AuthorService::class, function(ContainerInterface $c): AuthorService {
      return new AuthorService(
        $c->get(AuthorMapper::class)
      );
    });

    /**
     * Mappers
     */
    $context->registerService(AuthorMapper::class, function(ContainerInterface $c): AuthorMapper {
      return new AuthorMapper(
        $c->get(IDBConnection::class)
      );
    });
  }
}

How the container works

The container works in the following way:

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.

Which classes should be added

In general all of the app's controllers need to be registered inside the container. Then the following question is: What goes into the constructor of the controller? Pass everything into the controller constructor that matches one of the following criteria:

What not to inject:

Optional services

If an injected dependency can't be found or build, an exception is thrown. This can be avoided by using the a nullable type notation for a dependency:

If \Some\Service exists and can be built, it will be injected. Else MyService will receive null.

Accessing the container from anywhere

Sometimes it can be hard to inject some service inside legacy code, in these cases you can use OCP\Server::get(MyService::class). This should only be used as the last resort, as this makes your code more complicated to unit test and is considered an anti-pattern.