injection.txt 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. injection.txt
  2. This is an overview of how MediaWiki makes use of dependency injection.
  3. The design described here grew from the discussion of RFC T384.
  4. The term "dependency injection" (DI) refers to a pattern on object oriented
  5. programming that tries to improve modularity by reducing strong coupling
  6. between classes. In practical terms, this means that anything an object needs
  7. to operate should be injected from the outside, the object itself should only
  8. know narrow interfaces, no concrete implementation of the logic it relies on.
  9. The requirement to inject everything typically results in an architecture that
  10. based on two main types of objects: simple value objects with no business logic
  11. (and often immutable), and essentially stateless service objects that use
  12. other service objects to operate on the value objects.
  13. As of the beginning of 2016 (MW version 1.27), MediaWiki is only starting to
  14. use the DI approach. Much of the code still relies on global state or direct
  15. instantiation, resulting in a highly cyclical dependency graph.
  16. == Overview ==
  17. The heart of the DI in MediaWiki is the central service locator,
  18. MediaWikiServices, which acts as the top level factory for services in
  19. MediaWiki. MediaWikiServices::getInstance() returns the default service
  20. locator instance, which can be used to gain access to default instances of
  21. various services. MediaWikiServices however also allows new services to be
  22. defined and default services to be redefined. Services are defined or
  23. redefined by providing a callback function, the "instantiator" function,
  24. that will return a new instance of the service.
  25. When MediaWikiServices::getInstance() is first called, it will create an
  26. instance of MediaWikiServices and populate it with the services defined
  27. in the files listed by $wgServiceWiringFiles, thereby "bootstrapping" the
  28. DI framework. Per default, $wgServiceWiringFiles lists
  29. includes/ServiceWiring.php, which defines all default service
  30. implementations, and specifies how they depend on each other ("wiring").
  31. When a new service is added to MediaWiki core, an instantiator function
  32. that will create the appropriate default instance for that service must
  33. be added to ServiceWiring.php. This makes the service available through
  34. the generic getService() method on the service locator returned by
  35. MediaWikiServices::getInstance().
  36. Extensions can add their own wiring files to $wgServiceWiringFiles, in order
  37. to define their own service. Extensions may also use the 'MediaWikiServices'
  38. hook to define or redefined services by calling methods on the default
  39. MediaWikiServices instance.
  40. It should be noted that the term "service locator" is often used to refer to a
  41. top level factory that is accessed directly, throughout the code, to avoid
  42. explicit dependency injection. In contrast, the term "DI container" is often
  43. used to describe a top level factory that is only accessed when services
  44. are created. We use the term "service locator" for the top level factory
  45. because it is more descriptive than "DI container", even though application
  46. logic is strongly discouraged from accessing MediaWikiServices directly.
  47. MediaWikiServices::getInstance() should ideally be accessed only in "static
  48. entry points" such as hook handler functions. See "Migration" below.
  49. == Service Reset ==
  50. Services get their configuration injected, and changes to global
  51. configuration variables will not have any effect on services that were already
  52. instantiated. This would typically be the case for low level services like
  53. the ConfigFactory or the ObjectCacheManager, which are used during extension
  54. registration. To address this issue, Setup.php resets the global service
  55. locator instance by calling MediaWikiServices::resetGlobalInstance() once
  56. configuration and extension registration is complete.
  57. Note that "unmanaged" legacy services services that manage their own singleton
  58. must not keep references to services managed by MediaWikiServices, to allow a
  59. clean reset. After the global MediaWikiServices instance got reset, any such
  60. references would be stale, and using a stale service will result in an error.
  61. Services should either have all dependencies injected and be themselves managed
  62. by MediaWikiServices, or they should use the Service Locator pattern, accessing
  63. service instances via the global MediaWikiServices instance state when needed.
  64. This ensures that no stale service references remain after a reset.
  65. == Configuration ==
  66. When the default MediaWikiServices instance is created, a Config object is
  67. provided to the constructor. This Config object represents the "bootstrap"
  68. configuration which will become available as the 'BootstrapConfig' service.
  69. As of MW 1.27, the bootstrap config is a GlobalVarConfig object providing
  70. access to the $wgXxx configuration variables.
  71. The bootstrap config is then used to construct a 'ConfigFactory' service,
  72. which in turn is used to construct the 'MainConfig' service. Application
  73. logic should use the 'MainConfig' service (or a more specific configuration
  74. object). 'BootstrapConfig' should only be used for bootstrapping basic
  75. services that are needed to load the 'MainConfig'.
  76. Note: Several well known services in MediaWiki core act as factories
  77. themselves, e.g. ApiModuleManager, ObjectCache, SpecialPageFactory, etc.
  78. The registries these factories are based on are currently managed as part of
  79. the configuration. This may however change in the future.
  80. == Migration ==
  81. This section provides some recipes for improving code modularity by reducing
  82. strong coupling. The dependency injection mechanism described above is an
  83. essential tool in this effort.
  84. Migrate access to global service instances and config variables:
  85. Assume Foo is a class that uses the $wgScriptPath global and calls
  86. wfGetDB() to get a database connection, in non-static methods.
  87. * Add $scriptPath as a constructor parameter and use $this->scriptPath
  88. instead of $wgScriptPath.
  89. * Add LoadBalancer $dbLoadBalancer as a constructor parameter. Use
  90. $this->dbLoadBalancer->getConnection() instead of wfGetDB().
  91. * Any code that calls Foo's constructor would now need to provide the
  92. $scriptPath and $dbLoadBalancer. To avoid this, avoid direct instantiation
  93. of services all together - see below.
  94. Migrate class-level singleton getters:
  95. Assume class Foo has mostly non-static methods, and provides a static
  96. getInstance() method that returns a singleton (or default instance).
  97. * Add an instantiator function for Foo into ServiceWiring.php. The instantiator
  98. would do exactly what Foo::getInstance() did. However, it should
  99. replace any access to global state with calls to $services->getXxx() to get a
  100. service, or $services->getMainConfig()->get() to get a configuration setting.
  101. * Add a getFoo() method to MediaWikiServices. Don't forget to add the
  102. appropriate test cases in MediaWikiServicesTest.
  103. * Turn Foo::getInstance() into a deprecated alias for
  104. MediaWikiServices::getInstance()->getFoo(). Change all calls to
  105. Foo::getInstance() to use injection (see above).
  106. Migrate direct service instantiation:
  107. Assume class Bar calls new Foo().
  108. * Add an instantiator function for Foo into ServiceWiring.php and add a getFoo()
  109. method to MediaWikiServices. Don't forget to add the appropriate test cases
  110. in MediaWikiServicesTest.
  111. * In the instantiator, replace any access to global state with calls
  112. to $services->getXxx() to get a service, or $services->getMainConfig()->get()
  113. to get a configuration setting.
  114. * The code in Bar that calls Foo's constructor should be changed to have a Foo
  115. instance injected; Eventually, the only code that instantiates Foo is the
  116. instantiator in ServiceWiring.php.
  117. * As an intermediate step, Bar's constructor could initialize the $foo member
  118. variable by calling MediaWikiServices::getInstance()->getFoo(). This is
  119. acceptable as a stepping stone, but should be replaced by proper injection
  120. via a constructor argument. Do not however inject the MediaWikiServices
  121. object!
  122. Migrate parameterized helper instantiation:
  123. Assume class Bar creates some helper object by calling new Foo( $x ),
  124. and Foo uses a global singleton of the Xyzzy service.
  125. * Define a FooFactory class (or a FooFactory interface along with a MyFooFactory
  126. implementation). FooFactory defines the method newFoo( $x ) or getFoo( $x ),
  127. depending on the desired semantics (newFoo would guarantee a fresh instance).
  128. When Foo gets refactored to have Xyzzy injected, FooFactory will need a
  129. Xyzzy instance, so newFoo() can pass it to new Foo().
  130. * Add an instantiator function for FooFactory into ServiceWiring.php and add a
  131. getFooFactory() method to MediaWikiServices. Don't forget to add the
  132. appropriate test cases in MediaWikiServicesTest.
  133. * The code in Bar that calls Foo's constructor should be changed to have a
  134. FooFactory instance injected; Eventually, the only code that instantiates
  135. Foo are implementations of FooFactory, and the only code that instantiates
  136. FooFactory is the instantiator in ServiceWiring.php.
  137. * As an intermediate step, Bar's constructor could initialize the $fooFactory
  138. member variable by calling MediaWikiServices::getInstance()->getFooFactory().
  139. This is acceptable as a stepping stone, but should be replaced by proper
  140. injection via a constructor argument. Do not however inject the
  141. MediaWikiServices object!
  142. Migrate a handler registry:
  143. Assume class Bar calls FooRegistry::getFoo( $x ) to get a specialized Foo
  144. instance for handling $x.
  145. * Turn getFoo into a non-static method.
  146. * Add an instantiator function for FooRegistry into ServiceWiring.php and add
  147. a getFooRegistry() method to MediaWikiServices. Don't forget to add the
  148. appropriate test cases in MediaWikiServicesTest.
  149. * Change all code that calls FooRegistry::getFoo() statically to call this
  150. method on a FooRegistry instance. That is, Bar would have a $fooRegistry
  151. member, initialized from a constructor parameter.
  152. * As an intermediate step, Bar's constructor could initialize the $fooRegistry
  153. member variable by calling MediaWikiServices::getInstance()->
  154. getFooRegistry(). This is acceptable as a stepping stone, but should be
  155. replaced by proper injection via a constructor argument. Do not however
  156. inject the MediaWikiServices object!
  157. Migrate deferred service instantiation:
  158. Assume class Bar calls new Foo(), but only when needed, to avoid the cost of
  159. instantiating Foo().
  160. * Define a FooFactory interface and a MyFooFactory implementation of that
  161. interface. FooFactory defines the method getFoo() with no parameters.
  162. * Precede as for the "parameterized helper instantiation" case described above.
  163. Migrate a class with only static methods:
  164. Assume Foo is a class with only static methods, such as frob(), which
  165. interacts with global state or system resources.
  166. * Introduce a FooService interface and a DefaultFoo implementation of that
  167. interface. FooService contains the public methods defined by Foo.
  168. * Add an instantiator function for FooService into ServiceWiring.php and
  169. add a getFooService() method to MediaWikiServices. Don't forget to
  170. add the appropriate test cases in MediaWikiServicesTest.
  171. * Add a private static getFooService() method to Foo. That method just
  172. calls MediaWikiServices::getInstance()->getFooService().
  173. * Make all methods in Foo delegate to the FooService returned by
  174. getFooService(). That is, Foo::frob() would do self::getFooService()->frob().
  175. * Deprecate Foo. Inject a FooService into all code that calls methods
  176. on Foo, and change any calls to static methods in foo to the methods
  177. provided by the FooService interface.
  178. Migrate static hook handler functions (to allow unit testing):
  179. Assume MyExtHooks::onFoo is a static hook handler function that is called with
  180. the parameter $x; Further assume MyExt::onFoo needs service Bar, which is
  181. already known to MediaWikiServices (if not, see above).
  182. * Create a non-static doFoo( $x ) method in MyExtHooks that has the same
  183. signature as onFoo( $x ). Move the code from onFoo() into doFoo(), replacing
  184. any access to global or static variables with access to instance member
  185. variables.
  186. * Add a constructor to MyExtHooks that takes a Bar service as a parameter.
  187. * Add a static method called newFromGlobalState() with no parameters. It should
  188. just return new MyExtHooks( MediaWikiServices::getInstance()->getBar() ).
  189. * The original static handler method onFoo( $x ) is then implemented as
  190. self::newFromGlobalState()->doFoo( $x ).
  191. Migrate a "smart record":
  192. Assume Thingy is a "smart record" that "knows" how to load and store itself.
  193. For this purpose, Thingy uses wfGetDB().
  194. * Create a "dumb" value class ThingyRecord that contains all the information
  195. that Thingy represents (e.g. the information from a database row). The value
  196. object should not know about any service.
  197. * Create a DAO-style service for loading and storing ThingyRecords, called
  198. ThingyStore. It may be useful to split the interfaces for reading and
  199. writing, with a single class implementing both interfaces, so we in the
  200. end have the ThingyLookup and ThingyStore interfaces, and a SqlThingyStore
  201. implementation.
  202. * Add instantiator functions for ThingyLookup and ThingyStore in
  203. ServiceWiring.php. Since we want to use the same instance for both service
  204. interfaces, the instantiator for ThingyLookup would return
  205. $services->getThingyStore().
  206. * Add getThingyLookup() and getThingyStore methods to MediaWikiServices.
  207. Don't forget to add the appropriate test cases in MediaWikiServicesTest.
  208. * In the old Thingy class, replace all member variables that represent the
  209. record's data with a single ThingyRecord object.
  210. * In the old Thingy class, replace all calls to static methods or functions,
  211. such as wfGetDB(), with calls to the appropriate services, such as
  212. LoadBalancer::getConnection().
  213. * In Thingy's constructor, pull in any services needed, such as the
  214. LoadBalancer, by using MediaWikiServices::getInstance(). These services
  215. cannot be injected without changing the constructor signature, which
  216. is often impractical for "smart records" that get instantiated directly
  217. in many places in the code base.
  218. * Deprecate the old Thingy class. Replace all usages of it with one of the
  219. three new classes: loading needs a ThingyLookup, storing needs a ThingyStore,
  220. and reading data needs a ThingyRecord.
  221. Migrate lazy loading:
  222. Assume Thingy is a "smart record" as described above, but requires lazy loading
  223. of some or all the data it represents.
  224. * Instead of a plain object, define ThingyRecord to be an interface. Provide a
  225. "simple" and "lazy" implementations, called SimpleThingyRecord and
  226. LazyThingyRecord. LazyThingyRecord knows about some lower level storage
  227. interface, like a LoadBalancer, and uses it to load information on demand.
  228. * Any direct instantiation of a ThingyRecord would use the SimpleThingyRecord
  229. implementation.
  230. * SqlThingyStore however creates instances of LazyThingyRecord, and injects
  231. whatever storage layer service LazyThingyRecord needs to perform lazy loading.