docs/Injection.md
This is an overview of how MediaWiki uses of dependency injection. The design originates from RFC T384.
The term "dependency injection" (DI) refers to a pattern in object oriented programming. DI tries to improve modularity by reducing strong coupling between classes. In practical terms, this means that anything an object needs to operate should be injected from the outside. The object itself should only know narrow interfaces, no concrete implementation of the logic it relies on.
The requirement to inject everything typically results in an architecture based on two main kinds of objects: simple "value" objects with no business logic (and often immutable), and essentially stateless "service" objects that use other service objects to operate on value objects.
As of 2022 (MediaWiki 1.39), MediaWiki has adopted dependency injection in much of its code. However, some operations still require the use of singletons or otherwise involve global state.
The heart of the DI in MediaWiki is the central service locator, MediaWikiServices, which acts as the top-level factory (or registry) for services. MediaWikiServices represents the tree (or network) of service objects that define MediaWiki's application logic. It acts as an entry point to all dependency injection for MediaWiki core.
When MediaWikiServices::getInstance() is first called, it will create an
instance of MediaWikiServices and populate it with the services defined by
MediaWiki core in includes/ServiceWiring.php, as well as any additional
bootstrapping files specified in $wgServiceWiringFiles. The service
wiring files define the (default) service implementations to use, and
specifies how they depend on each other ("wiring").
Extensions can add their own wiring files to $wgServiceWiringFiles, in order
to define their own service. Extensions may also use the MediaWikiServices
hook to replace ("redefine") a core service, by calling methods on the
MediaWikiServices instance.
It should be noted that the term "service locator" is often used to refer to a top-level factory that is accessed directly, throughout the code, to avoid explicit dependency injection. In contrast, the term "DI container" is often used to describe a top-level factory that is only accessed only inside service wiring code when instantiating service classes. We use the term "service locator" because it is more descriptive than "DI container", even though application logic is strongly discouraged from accessing MediaWikiServices directly.
MediaWikiServices::getInstance() should ideally be accessed only in "static
entry points" such as hook handler functions. See "Migration" below.
Service classes generally only vary on site configuration and are deterministic and agnostic of global state. It is the responsibility of callers to a service object to obtain and derive information from a web request (such as title, user, language, WebRequest, RequestContext), and pass this to specific methods of a service class as-needed. See T218555 for related discussion.
Consider using the factory pattern if your service would otherwise be unergonomic or slow, e.g. due to passing many parameters and/or recomputing the same derived information. This keeps the global state out of the service class, by having the service be a factory from which the caller can obtain a (re-usable) object for its specific context.
This design ensures service classes are safe to use in both user-facing contexts on the web (e.g. index.php page views and special pages), as well as in an API, job, or maintenance script. It also ensures that within a web-facing context the same service can be safely used multiple times to perform different operations, without incorrectly implying certain commonalities between these calls. Lastly, this restriction allows services to be instantiated across wikis in the future.
If a feature is not ready to meet these requirements, keep it outside the service container. This avoids false confidence in the safety of an injected service, and its ripple effect on other services.
There is a limited exemption to the above principles for "inconsequential state". That is, global state may be used directly if and only if used for diagnostics or to optimise performance, so long as they do not change the observed functional outcome of a called method.
Examples of safe and inconsequential state:
Use $_SERVER['REQUEST_TIME_FLOAT'] or ConvertibleTimestamp::now
to help compute a time measure that is sent to a metric service.
Use wfHostname(), PHP_SAPI, or WikiMap::getCurrentWikiId()
to describe where, how, or for which wiki the overall process was
created and send it as message context to a logging service.
Use WebRequest::getRequestId() to automatically inject a
header into HTTP requests to other services. These are for tracking
purposes only.
Use function_exists('apcu_fetch') to automatically enable use
of caching.
Examples of unsafe state in a service class:
Do not use WikiMap::getCurrentWikiId() as the default value
to obtain a database connection.
Do not use $_SERVER['SERVER_NAME'] to inject a header into
HTTP requests to other services to control which wiki to operate on.
To create a new service in MediaWiki core, write a function that will return
the appropriate class instantiation for that service in ServiceWiring.php. This
makes the service available through the generic getService() method on the
MediaWikiServices class. We then also add a wrapper method to
MediaWikiServices.php with a discoverable method named and strictly typed
return value to reduce mistakes and improve static analysis.
Services get their configuration injected, and changes to global
configuration variables will not have any effect on services that were already
instantiated. This would typically be the case for low level services like
the ConfigFactory or the ObjectCacheManager, which are used during extension
registration. To address this issue, Setup.php resets the global service
locator instance by calling MediaWikiServices::resetGlobalInstance() once
configuration and extension registration is complete.
Note that "unmanaged" legacy services services that manage their own singleton must not keep references to services managed by MediaWikiServices, to allow a clean reset. After the global MediaWikiServices instance got reset, any such references would be stale, and using a stale service will result in an error.
Services should either have all dependencies injected and be themselves managed by MediaWikiServices, or they should use the Service Locator pattern, accessing service instances via the global MediaWikiServices instance state when needed. This ensures that no stale service references remain after a reset.
When the default MediaWikiServices instance is created, a Config object is provided to the constructor. This Config object represents the "bootstrap" configuration which will become available as the 'BootstrapConfig' service. As of MW 1.27, the bootstrap config is a GlobalVarConfig object providing access to the $wgXxx configuration variables.
The bootstrap config is then used to construct a 'ConfigFactory' service, which in turn is used to construct the 'MainConfig' service. Application logic should use the 'MainConfig' service (or a more specific configuration object). 'BootstrapConfig' should only be used for bootstrapping basic services that are needed to load the 'MainConfig'.
Note: Several well known services in MediaWiki core act as factories themselves, e.g. ApiModuleManager, ObjectCache, SpecialPageFactory, etc. The registries these factories are based on are currently managed as part of the configuration. This may however change in the future.
This section provides some recipes for improving code modularity by reducing strong coupling. The dependency injection mechanism described above is an essential tool in this effort.
Assume Foo is a class that uses the $wgScriptPath global and calls
wfGetDB() to get a database connection, in non-static methods.
$scriptPath as a constructor parameter and use $this->scriptPath
instead of $wgScriptPath.$dbProvider as a constructor parameter. Use
$this->dbProvider->getReplicaDatabase() instead of wfGetDB( DB_REPLICA ),
$this->dbProvider->->getPrimaryDatabase() instead of wfGetDB( DB_PRIMARY ).Foo's constructor would now need to provide the
$scriptPath and $dbProvider. To avoid this, avoid direct instantiation
of services all together - see below.When a service needs multiple configuration globals injected, a ServiceOptions
object is commonly used with the service class defining a public constant
(usually CONSTRUCTOR_OPTIONS) with an array of settings that the class needs
access to.
<?php
class DemoService {
public const CONSTRUCTOR_OPTIONS = [
'Foo',
'Bar',
];
public function __construct( private readonly ServiceOptions $options ) {
// ServiceOptions::assertRequiredOptions ensures that all of the
// settings listed in CONSTRUCTOR_OPTIONS are available
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
// $wgFoo is now available with $this->options->get( 'Foo' )
// $wgBar is now available with $this->options->get( 'Bar' )
}
}
ServiceOptions objects are constructed within ServiceWiring.php and can also be created in tests.
'DemoService' => static function ( MediaWikiServices $services ): DemoService {
return new DemoService(
new ServiceOptions(
DemoService::CONSTRUCTOR_OPTIONS,
$services->getMainConfig()
),
);
},
Assume class Foo has mostly non-static methods, and provides a static
getInstance() method that returns a singleton (or default instance).
Foo into ServiceWiring.php. The
instantiator would do exactly what Foo::getInstance() did. However, it
should replace any access to global state with calls to $services->getXxx()
to get a service, or $services->getMainConfig()->get() to get a
configuration setting.getFoo() method to MediaWikiServices. Don't forget to add the
appropriate test cases in MediaWikiServicesTest.Foo::getInstance() into a deprecated alias for
MediaWikiServices::getInstance()->getFoo(). Change all calls to
Foo::getInstance() to use injection (see above).Assume class Bar calls new Foo().
Foo into ServiceWiring.php and add a
getFoo() method to MediaWikiServices. Don't forget to add the appropriate
test cases in MediaWikiServicesTest.$services->getXxx() to get a service, or
$services->getMainConfig()->get() to get a configuration setting.Bar that calls Foo's constructor should be changed to have a
Foo instance injected; Eventually, the only code that instantiates Foo is
the instantiator in ServiceWiring.php.Bar's constructor could initialize the $foo
member variable by calling MediaWikiServices::getInstance()->getFoo(). This
is acceptable as a stepping stone, but should be replaced by proper injection
via a constructor argument. Do not however inject the MediaWikiServices
object!Assume class Bar creates some helper object by calling new Foo( $x ),
and Foo uses a global singleton of the Xyzzy service.
FooFactory class (or a FooFactory interface along with a
MyFooFactory implementation). FooFactory defines the method
newFoo( $x ) or getFoo( $x ), depending on the desired semantics (newFoo
would guarantee a fresh instance). When Foo gets refactored to have Xyzzy
injected, FooFactory will need a Xyzzy instance, so newFoo() can pass it
to new Foo().MediaWikiServices::getInstance()->getFooFactory().
This is acceptable as a stepping stone, but should be replaced by proper
injection via a constructor argument. Do not however inject the
MediaWikiServices object!Assume class Bar calls FooRegistry::getFoo( $x ) to get a specialized Foo
instance for handling $x.
getFoo into a non-static method.FooRegistry into ServiceWiring.php and add
a getFooRegistry() method to MediaWikiServices. Don't forget to add the
appropriate test cases in MediaWikiServicesTest.FooRegistry::getFoo() statically to call this
method on a FooRegistry instance. That is, Bar would have a $fooRegistry
member, initialized from a constructor parameter.$fooRegistry
member variable by calling
MediaWikiServices::getInstance()->getFooRegistry(). This is acceptable as a
stepping stone, but should be replaced by proper injection via a constructor
argument. Do not however inject the MediaWikiServices object!Assume class Bar calls new Foo(), but only when needed, to avoid the cost of
instantiating Foo().
FooFactory interface and a MyFooFactory implementation of that
interface. FooFactory defines the method getFoo() with no parameters.Assume Foo is a class with only static methods, such as frob(), which
interacts with global state or system resources.
FooService interface and a DefaultFoo implementation of that
interface. FooService contains the public methods defined by Foo.FooService into ServiceWiring.php and
add a getFooService() method to MediaWikiServices. Don't forget to
add the appropriate test cases in MediaWikiServicesTest.getFooService() method to Foo. That method just
calls MediaWikiServices::getInstance()->getFooService().Foo delegate to the FooService returned by
getFooService(). That is, Foo::frob() would do
self::getFooService()->frob().Foo. Inject a FooService into all code that calls methods
on Foo, and change any calls to static methods in foo to the methods
provided by the FooService interface.Assume MyExtHooks::onFoo is a static hook handler function that is called with
the parameter $x; Further assume MyExt::onFoo needs service Bar, which is
already known to MediaWikiServices (if not, see above).
doFoo( $x ) method in MyExtHooks that has the same
signature as onFoo( $x ). Move the code from onFoo() into doFoo(),
replacing any access to global or static variables with access to instance
member variables.MyExtHooks that takes a Bar service as a parameter.newFromGlobalState() with no parameters. It
should just return
new MyExtHooks( MediaWikiServices::getInstance()->getBar() ).onFoo( $x ) is then implemented as
self::newFromGlobalState()->doFoo( $x ).Assume Thingy is a "smart record" that "knows" how to load and store itself.
For this purpose, Thingy uses wfGetDB().
ThingyRecord that contains all the information
that Thingy represents (e.g. the information from a database row). The value
object should not know about any service.ThingyRecords, called
ThingyStore. It may be useful to split the interfaces for reading and
writing, with a single class implementing both interfaces, so we in the
end have the ThingyLookup and ThingyStore interfaces, and a SqlThingyStore
implementation.ThingyLookup and ThingyStore in
ServiceWiring.php. Since we want to use the same instance for both service
interfaces, the instantiator for ThingyLookup would return
$services->getThingyStore().getThingyLookup() and getThingyStore() methods to MediaWikiServices.
Don't forget to add the appropriate test cases in MediaWikiServicesTest.Thingy class, replace all member variables that represent the
record's data with a single ThingyRecord object.IConnectionProvider::getReplicaDatabase().MediaWikiServices::getInstance(). These services
cannot be injected without changing the constructor signature, which
is often impractical for "smart records" that get instantiated directly
in many places in the code base.Thingy class. Replace all usages of it with one of the
three new classes: loading needs a ThingyLookup, storing needs a
ThingyStore, and reading data needs a ThingyRecord.Assume Thingy is a "smart record" as described above, but requires lazy
loading of some or all the data it represents.
ThingyRecord to be an interface. Provide a
"simple" and "lazy" implementations, called SimpleThingyRecord and
LazyThingyRecord. LazyThingyRecord knows about some lower level storage
interface, like a LoadBalancer, and uses it to load information on demand.ThingyRecord would use the
SimpleThingyRecord implementation.SqlThingyStore however creates instances of LazyThingyRecord, and injects
whatever storage layer service LazyThingyRecord needs to perform lazy
loading.