Update references from .txt to .md when files have moved, a few other tweaks, no changes to code. Change-Id: I0bfd38c47b9fb0fc11ae98a0a674af66fb4c5a84
15 KiB
Depencency Injection
This is an overview of how MediaWiki makes use of dependency injection. The design described here grew from the discussion of RFC T384.
The term "dependency injection" (DI) refers to a pattern on object oriented programming that 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 that based on two main types 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 the value objects.
As of the beginning of 2016 (MW version 1.27), MediaWiki is only starting to use the DI approach. Much of the code still relies on global state or direct instantiation, resulting in a highly cyclical dependency graph.
Overview
The heart of the DI in MediaWiki is the central service locator,
MediaWikiServices, which acts as the top level factory for services in
MediaWiki. MediaWikiServices::getInstance() returns the default service
locator instance, which can be used to gain access to default instances of
various services. MediaWikiServices however also allows new services to be
defined and default services to be redefined. Services are defined or
redefined by providing a callback function, the "instantiator" function,
that will return a new instance of the service.
When MediaWikiServices::getInstance() is first called, it will create an
instance of MediaWikiServices and populate it with the services defined
in the files listed by $wgServiceWiringFiles, thereby "bootstrapping" the
DI framework. Per default, $wgServiceWiringFiles lists
includes/ServiceWiring.php, which defines all default service
implementations, and specifies how they depend on each other ("wiring").
When a new service is added to MediaWiki core, an instantiator function
that will create the appropriate default instance for that service must
be added to ServiceWiring.php. This makes the service available through
the generic getService() method on the service locator returned by
MediaWikiServices::getInstance().
Extensions can add their own wiring files to $wgServiceWiringFiles, in order
to define their own service. Extensions may also use the 'MediaWikiServices'
hook to define or redefined services by calling methods on the default
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 when services
are created. We use the term "service locator" for the top level factory
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 Reset
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.
Configuration
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.
Migration
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.
Migrate access to global service instances and config variables
Assume Foo is a class that uses the $wgScriptPath global and calls
wfGetDB() to get a database connection, in non-static methods.
- Add
$scriptPathas a constructor parameter and use$this->scriptPathinstead of$wgScriptPath. - Add LoadBalancer
$dbLoadBalanceras a constructor parameter. Use$this->dbLoadBalancer->getConnection()instead ofwfGetDB(). - Any code that calls
Foo's constructor would now need to provide the$scriptPathand$dbLoadBalancer. To avoid this, avoid direct instantiation of services all together - see below.
Migrate class-level singleton getters
Assume class Foo has mostly non-static methods, and provides a static
getInstance() method that returns a singleton (or default instance).
- Add an instantiator function for
Foointo ServiceWiring.php. The instantiator would do exactly whatFoo::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. - Add a
getFoo()method to MediaWikiServices. Don't forget to add the appropriate test cases in MediaWikiServicesTest. - Turn
Foo::getInstance()into a deprecated alias forMediaWikiServices::getInstance()->getFoo(). Change all calls toFoo::getInstance()to use injection (see above).
Migrate direct service instantiation
Assume class Bar calls new Foo().
- Add an instantiator function for
Foointo ServiceWiring.php and add agetFoo()method to MediaWikiServices. Don't forget to add the appropriate test cases in MediaWikiServicesTest. - In the instantiator, replace any access to global state with calls
to
$services->getXxx()to get a service, or$services->getMainConfig()->get()to get a configuration setting. - The code in
Barthat callsFoo's constructor should be changed to have aFooinstance injected; Eventually, the only code that instantiatesFoois the instantiator in ServiceWiring.php. - As an intermediate step,
Bar's constructor could initialize the$foomember variable by callingMediaWikiServices::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!
Migrate parameterized helper instantiation
Assume class Bar creates some helper object by calling new Foo( $x ),
and Foo uses a global singleton of the Xyzzy service.
- Define a
FooFactoryclass (or aFooFactoryinterface along with aMyFooFactoryimplementation).FooFactorydefines the methodnewFoo( $x )orgetFoo( $x ), depending on the desired semantics (newFoowould guarantee a fresh instance). When Foo gets refactored to haveXyzzyinjected,FooFactorywill need aXyzzyinstance, sonewFoo()can pass it tonew Foo(). - Add an instantiator function for FooFactory into ServiceWiring.php and add a getFooFactory() method to MediaWikiServices. Don't forget to add the appropriate test cases in MediaWikiServicesTest.
- The code in Bar that calls Foo's constructor should be changed to have a FooFactory instance injected; Eventually, the only code that instantiates Foo are implementations of FooFactory, and the only code that instantiates FooFactory is the instantiator in ServiceWiring.php.
- As an intermediate step, Bar's constructor could initialize the $fooFactory
member variable by calling
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!
Migrate a handler registry
Assume class Bar calls FooRegistry::getFoo( $x ) to get a specialized Foo
instance for handling $x.
- Turn
getFoointo a non-static method. - Add an instantiator function for
FooRegistryinto ServiceWiring.php and add agetFooRegistry()method to MediaWikiServices. Don't forget to add the appropriate test cases in MediaWikiServicesTest. - Change all code that calls
FooRegistry::getFoo()statically to call this method on aFooRegistryinstance. That is,Barwould have a$fooRegistrymember, initialized from a constructor parameter. - As an intermediate step, Bar's constructor could initialize the
$fooRegistrymember variable by callingMediaWikiServices::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!
Migrate deferred service instantiation
Assume class Bar calls new Foo(), but only when needed, to avoid the cost of
instantiating Foo().
- Define a
FooFactoryinterface and aMyFooFactoryimplementation of that interface.FooFactorydefines the methodgetFoo()with no parameters. - Precede as for the "parameterized helper instantiation" case described above.
Migrate a class with only static methods
Assume Foo is a class with only static methods, such as frob(), which
interacts with global state or system resources.
- Introduce a
FooServiceinterface and aDefaultFooimplementation of that interface.FooServicecontains the public methods defined by Foo. - Add an instantiator function for
FooServiceinto ServiceWiring.php and add agetFooService()method to MediaWikiServices. Don't forget to add the appropriate test cases in MediaWikiServicesTest. - Add a private static
getFooService()method toFoo. That method just callsMediaWikiServices::getInstance()->getFooService(). - Make all methods in
Foodelegate to theFooServicereturned bygetFooService(). That is,Foo::frob()would doself::getFooService()->frob(). - Deprecate
Foo. Inject aFooServiceinto all code that calls methods onFoo, and change any calls to static methods in foo to the methods provided by theFooServiceinterface.
Migrate static hook handler functions (to allow unit testing)
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).
- Create a non-static
doFoo( $x )method inMyExtHooksthat has the same signature asonFoo( $x ). Move the code fromonFoo()intodoFoo(), replacing any access to global or static variables with access to instance member variables. - Add a constructor to
MyExtHooksthat takes a Bar service as a parameter. - Add a static method called
newFromGlobalState()with no parameters. It should just returnnew MyExtHooks( MediaWikiServices::getInstance()->getBar() ). - The original static handler method
onFoo( $x )is then implemented asself::newFromGlobalState()->doFoo( $x ).
Migrate a "smart record"
Assume Thingy is a "smart record" that "knows" how to load and store itself.
For this purpose, Thingy uses wfGetDB().
- Create a "dumb" value class
ThingyRecordthat contains all the information thatThingyrepresents (e.g. the information from a database row). The value object should not know about any service. - Create a DAO-style service for loading and storing
ThingyRecords, calledThingyStore. 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 theThingyLookupandThingyStoreinterfaces, and a SqlThingyStore implementation. - Add instantiator functions for
ThingyLookupandThingyStorein ServiceWiring.php. Since we want to use the same instance for both service interfaces, the instantiator forThingyLookupwould return$services->getThingyStore(). - Add
getThingyLookup()andgetThingyStore()methods to MediaWikiServices. Don't forget to add the appropriate test cases in MediaWikiServicesTest. - In the old
Thingyclass, replace all member variables that represent the record's data with a singleThingyRecordobject. - In the old Thingy class, replace all calls to static methods or functions,
such as wfGetDB(), with calls to the appropriate services, such as
LoadBalancer::getConnection(). - In Thingy's constructor, pull in any services needed, such as the
LoadBalancer, by using
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. - Deprecate the old
Thingyclass. Replace all usages of it with one of the three new classes: loading needs aThingyLookup, storing needs aThingyStore, and reading data needs aThingyRecord.
Migrate lazy loading
Assume Thingy is a "smart record" as described above, but requires lazy
loading of some or all the data it represents.
- Instead of a plain object, define
ThingyRecordto be an interface. Provide a "simple" and "lazy" implementations, calledSimpleThingyRecordandLazyThingyRecord.LazyThingyRecordknows about some lower level storage interface, like a LoadBalancer, and uses it to load information on demand. - Any direct instantiation of a
ThingyRecordwould use theSimpleThingyRecordimplementation. SqlThingyStorehowever creates instances ofLazyThingyRecord, and injects whatever storage layer serviceLazyThingyRecordneeds to perform lazy loading.