Dependency Injection Container - markstory/cakephp GitHub Wiki

CakePHP has long been without a dependency injection container, as we haven't needed one in CakePHP core. Instead of using a DI container we rely on service locators for the ORM, global configuration and constructor/method parameters instead. While CakePHP hasn't needed a dependency injection container directly it would be useful for application developers.

Problems a Container Solves

As applications increase in complexity they need ways to centralize object creation, reduce coupling to dependencies, and have a central place to build reusable objects and the relationships between them.

A container solves these problems by acting as a configurable factory for a variety of objects. By fetching objects from the container, the container can resolve dependency trees based on the configured state. Having a central factory makes swapping implementations for testing or refactoring simpler as all the creation logic is located in one place.

Proposed Implementation

This proposal does not include the creation of a container implementation. Instead, we should re-use an existing container based on PSR-11. Using a PSR-11 container and coupling to those interfaces allow us to change container libraries more easily in the future if necessary.

High Level Goals

A dependency injection container in CakePHP should enable applications and plugins to define services and then have those services injected into controllers and commands as requested. Views, Helpers, Tables, Mailers and Behaviors will not have container services injected into them. These objects are generally created far away from the Application and thus hard to inject into. Furthermore, none of these class types are 'entrypoints' into an application and could have services passed into their methods as parameters by the top level 'entrypoint' classes (controllers and commands).

The container would be initialized during Application construction. Services could be registered during the new register() hook which would be called after bootstrap(). Calling register() after bootstrap() lets us ensure that the application and all plugins have initialized their configuration and constants before any services are defined.

Registering Services

Services would be registered during the register() hook method implemented on both the BaseApplication and BasePlugin classes. Application classes would implement a new interface, (ContainerApplicationInterface). This interface would define the following methods:

  • register(ContainerInterface $container): ContainerInterface - Implemented by applications to register their services.
  • getContainer(): ContainerInterface - Creates the new container if one has not been created, registers all application and plugin services. Once the container is prepared references to the container will be returned.

Having a new interface ensures that container features are opt-in for standalone HTTP and console applications.

Plugin Services

Plugins would be able to register services in an application's container. Plugins would have a register hook method called after the application services are registered.

register(ContainerInterface $container): ContainerInterface

Would be the signature of BasePlugin::register(). The register() method would be implemented on BasePlugin and added to the PluginInterface via a @method annotation because of backwards compatibility. Adding another interface is an alternative approach but I'd prefer to avoid it as it adds complexity and most plugins inherit from BasePlugin anyways.

The PluginInterface::VALID_HOOKS constant would be updated to include the register value. This is required to enable with('register') to work.

Tagged and Deferred Services

Most of the container options listed below offer the creation of deferred and tagged services, which enables applications with many services to improve performance and customization.

Configuration Service

If possible, CakePHP would register the state of Configure as a 'service'. Doing so allows services to access configuration information without having to refer to Configure. Because DI containers are class/interface based, configuration data would be wrapped in a wrapper class (Config, ConfigData or Configuration) that includes the following methods:

  • has(string $key): bool Test for a value.
  • get(string $key, $default = null): mixed Read a value.

Both of these methods would support the simple . based traversal that Configure::read() does.

Getting Services

Controllers and Commands would receive registered services as constructor arguments or parameters to action methods. The construction of controllers and commands is contained within factory classes which can both easily be passed Application references. The same is true of command execution and controller action invocation.

An example controller action using injection would be:

function index(RepositoryService $repos)
{
  // RepositoryService would be injected from the container.
  $records = $repos->findAll($user);
}

If a controller action accepts passed parameters, services would be appended to the end of the argument list:

function edit(int $pullRequestId, int $commentId, RepositoryService $repos)
{
  // method body.
}

Container Library Options

A short list of options is: