Requests for comment/Dependency injection

This RFC proposes best practices for establishing the dependency injection pattern in MediaWiki without relying on a DI framework.

Proposal Summary
This RFC proposes the following to be adopted as best practice: These principles effectively provide dependency injection "on foot", without the need for a DI framework or a declarative syntax for networks of service objects. Decisions regarding the life cycle of service objects (lazy initialization, etc) are left to plain old PHP code.
 * 1) When a service (or application logic) needs access to another a service, it asks for it in the constructor. This is the actual injection of dependencies.
 * 2) Objects that need access to services can only be constructed via factories (not directly using the new operator).
 * 3) Services are constructed by other services (factories and registries being special types of service). At the top of this chain of registries/factories there is the application scope service locator which acts as the top level service registry.
 * 4) Access to global default instances ("singletons") should be restricted to static entry points (e.g. hook handlers and callbacks in bootstrap code). Ideally, there is only one such global default instance, namely the service locator.
 * 5) Injecting/passing around factories, registries, and especially "kitchen sinks" like RequestContent should be avoided. The service locator should never be passed as a parameter.
 * 6) Mutable global state should especially be avoided.
 * 7) Services should be represented by narrow interfaces (e.g. UserLookup).
 * 8) Registries use constructor callbacks (aka factory functions) to instantiate services.
 * 9) Bootstrap code should avoid instantiating services, but define constructor callback instead.

Rationale
Dependency injection (DI) is a design pattern that can facilitate unit testing, loose coupling and architecture description. Although it's more useful in some languages than in others, it is a well-established pattern, and there is a solid ecosystem of DI libraries for PHP.

MediaWiki doesn't have a dedicated DI mechanism, though adding one has been discussed.

Adding simple DI support to core would be a first step towards consistent, concise use of this pattern. Since we'll probably need at least a few iterations and use cases to get it right, this first step could be a kind of "internal API beta feature".

See also: Requests for comment/Services and narrow interfaces

Previous Discussions
Using DI in MediaWiki has been considered before. Here are some earlier conversations about it:
 * Previous incarnation of this RFC
 * Discussion of changes to Architecture guidelines at Wikimania 2013
 * Section on DI for external resources in Talk:Architecture guidelines
 * Discussion of TitleValue at the Arcitecture Summit 2014
 * The TitleValue RFC and the ServiceRegistry section on that RFC's Talk page

Static Entry Points
A static entry point is code in a static context (directly in clobal scope, or in a global function, or in a static method) that gets called by a framework (e.g. the PHP runtime, or MediaWiki's Hooks mechanism). In MediaWiki, typical static entry points are:


 * 1) Global scope code in web entry points like index.php, load.php, thumb.php, etc.
 * 2) Global scope code in maintenance scripts.
 * 3) Extension bootstrap code (see )
 * 4) Hook handler functions (see )
 * 5) Constructor callbacks

Service Locator
The application scope service locator is the top level registry for the services that the application's logic needs to operate (see e.g. ). Extensions can define their own service locators (which may depend on MediaWiki's service locator), see e.g..

Bootstrap Code
Bootstrap code refers to code that is executed at the beginning of every request. Bootstrap code creates the initial scaffolding for initializing the application by loading configuration and instantiating the most basic services. In MediaWiki, bootstrap code is typically:
 * 1) global scope code in a web entry point (or maintenance script).
 * 2) Extension entry points (see )

Code inside hook handler functions or Constructor callbacks is not bootstrap code, since it is not executed during the initialization process.

Factory
A factory is a service that instantiates objects. These objects can be services, or data objects. Factory methods that are guaranteed to create a new instance should have names starting with "new". Other factory methods should have names starting with "get", and may or may not return singletons.

Factories are used to inject the ability to instantiate certain kinds of objects. They can be understood as partial applications of constructors. Factory methods typically, but not necessarily, take parameters.

A "factory" in the more narrow sense would typically have only one factory method, and create only one kind of object.

Registry
Registries are factories for services. Factory methods in a registry typically do not take any parameters. Registries can be used to
 * 1) provide access to instances of a variety of services, e.g. various storage level services.
 * 2) provide access to specialized instances of a single service interface implemented for different "targets", e.g. different MediaHandler instances for each type of media.
 * 3) provide lazy instantiation of services, to avoid overhead at startup time.

The top level registry (the service locator) provides access to all services known to the application, for use in static entry points.

A registry may be implemented by hardcoding the logic for instantiating the services (typical especially for the top level registry), or by bootstrap code defining constructor callbacks (aka factory functions). See the example. Note that registering class names should be avoided, since that prevents injection of services via constructor arguments (because the constructor's signature is prescribed by the registry).

Constructor Callback
A constructor callback or factory function is a callable that returns a new instance of some class.

UserLookup Interface
Note that this is an interface for looking up users. It does not contain methods for updating user records, nor for creating new users.

MedaWikiServices
To avoid boilerplate code for lazy instantiation, this can be generalized a bit:

Hook Handler Injection
The handler function is then hooked up as usual:

The static logic can also be moved into an anonymous function, if preferred:

This is somewhat cleaner, since spurious dependencies in the handler class are avoided. But keeping the static code in the handler class provides better knowledge locality, and avoids clutter in the bootstrap file.