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:
- https://container.thephpleague.com/3.x/
- http://php-di.org
- TODO get more