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