Dependency Injection

From MediaWiki.org
Jump to: navigation, search

This page describes the use of the Dependency Injection (DI) facility provided by the MediaWikiServices class.

Quick Start[edit]

This section provides a quick start to those who are already familiar with DI.

Using services:

  • To acquire a reference to the service container, use MediaWikiServices::getInstance(). This should only be done in bootstrap code and static entry points. Accessing the service container so in application logic (as per the Service Locator pattern) or passing the service container as a parameter or keeping a reference to the service container in a member variable (as per the Context Object pattern) is strongly discouraged.
  • To acquire an instance of a well known service, use a dedicated getter on the service container object, e.g. getMainConfig() or getDBLoadBalancer().
  • To acquire an instance of a custom service defined by an extension, use the generic getService( $name ) method. Extensions may provide static convenience functions of the form MyExtensionServices::getFooService() implemented to return MediaWikiServices::getInstance()->getService( 'MyExtensionFoo' );
  • To temporarily override a service for testing, use the setService( $name, $service ) method of MediaWikiTestCase.

Defining services:

  • To add a new well known service to MediaWiki core:
    • add an instantiator callback to includes/ServiceWiring.php that returns a new instance of the desired service class.
    • add a getter to MediaWikiServices that corresponds to the service name used in the wiring file. The getter should be implemented trivially implemented to return $this->getService( 'SomeServiceName' ).
    • add the new service name and expected class to the list in MediaWikiServicesTest::provideGetService. Note that the expected class name should refer to the service interface, not the concrete service implementation.
  • To register a custom service using the MediaWikiServices hook:
    • Register a hander for the MediaWikiServices hook. The handler function should expect a MediaWikiServices object as the only parameter. To avoid name conflicts, the service name should be prefixed with the extension's name.
    • In the handler function, call defineService(), providing an instantiator callback for the custom service which returns a new instance of the desired service class.
  • To register a custom service via the ServiceWiringFiles registry:
    • Create a wiring file, e.g. wiring.php, in the extension directory. The wiring file should consist of a single return statement, returning an associative array that maps service names to instantiator functions. To avoid name conflicts, the service names should be prefixed with the extension's name.
    • Register the wiring file in the ServiceWiringFiles section of the extension.json file, or add it to $wgServiceWiringFiles in the extension's bootstrap code.
  • To re-use the same service instance for multiple service names:
    • register the instantiator for the implementation under an "internal" service name (by convention, use a name starting with an underscore "_")
    • for each name the service should be available under, register an pseudo-instantiator that just calls getService() with the internal service name.

Concepts and Best Practices[edit]

Best practices for using Dependency Injection in MediWiki:

  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 container 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 container.
  5. Injecting/passing around factories, registries, and especially "kitchen sinks" like RequestContext should be avoided. The service container should never be passed as a parameter.
  6. Mutable global state should especially be avoided.
  7. Services should be represented by narrow interfaces.
  8. Registries use instantiator functions to instantiate services.
  9. Bootstrap code should avoid instantiating services, but define instantiator function instead.

These principles effectively promote dependency injection, without binding to a particular DI framework. In fact, it allows DI to be implemented entirely "on foot", without any configurable DI container - instantiation and decisions regarding the life cycle of service objects (lazy initialization, etc) can be left to plain old PHP code.

TBD: DI vs Service Locator

TBD: DI vs Container injection (context objects)

Glossary[edit]

Static entry points[edit]

A static entry point is code in a static context (directly in global 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. Bootstrap code in old-style extension entry points
  4. (Static) hook handler functions
  5. Instantiator functions in wiring files

Service Container[edit]

The application-scope service container is the top-level registry for the services that the application's logic needs to operate. Extensions can define their own service containers (which may depend on MediaWiki's service container).

Access to the service container should be restricted to static entry points. Direct access to the global service container instance from application logic following the service locator pattern is discouraged. See Assembly comparison for a discussion of service locator vs. DI container logic.

Bootstrap code[edit]

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 #Registry bootstrap)

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

Factory[edit]

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[edit]

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 container) 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 instatiator functions (aka factory functions). See the #Registry bootstrap 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).

Instantiator function[edit]

A instantiator function (or factory callback) is a callable that returns a new instance of some class.

Service Wiring[edit]

TBD

The MediaWikiServices class[edit]

TBD

Registering Services[edit]

TBD

Using Dependency Injection in Hook Handlers[edit]

TBD

Mocking Servics for Testing[edit]

TBD

References[edit]

Original RFC: Requests for comment/Dependency injection