Fix php code style
[lhc/web/wiklou.git] / includes / services / ServiceContainer.php
1 <?php
2 namespace MediaWiki\Services;
3
4 use InvalidArgumentException;
5 use RuntimeException;
6 use Wikimedia\Assert\Assert;
7
8 /**
9 * Generic service container.
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License along
22 * with this program; if not, write to the Free Software Foundation, Inc.,
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24 * http://www.gnu.org/copyleft/gpl.html
25 *
26 * @file
27 *
28 * @since 1.27
29 */
30
31 /**
32 * ServiceContainer provides a generic service to manage named services using
33 * lazy instantiation based on instantiator callback functions.
34 *
35 * Services managed by an instance of ServiceContainer may or may not implement
36 * a common interface.
37 *
38 * @note When using ServiceContainer to manage a set of services, consider
39 * creating a wrapper or a subclass that provides access to the services via
40 * getter methods with more meaningful names and more specific return type
41 * declarations.
42 *
43 * @see docs/injection.txt for an overview of using dependency injection in the
44 * MediaWiki code base.
45 */
46 class ServiceContainer implements DestructibleService {
47
48 /**
49 * @var object[]
50 */
51 private $services = [];
52
53 /**
54 * @var callable[]
55 */
56 private $serviceInstantiators = [];
57
58 /**
59 * @var boolean[] disabled status, per service name
60 */
61 private $disabled = [];
62
63 /**
64 * @var array
65 */
66 private $extraInstantiationParams;
67
68 /**
69 * @var boolean
70 */
71 private $destroyed = false;
72
73 /**
74 * @param array $extraInstantiationParams Any additional parameters to be passed to the
75 * instantiator function when creating a service. This is typically used to provide
76 * access to additional ServiceContainers or Config objects.
77 */
78 public function __construct( array $extraInstantiationParams = [] ) {
79 $this->extraInstantiationParams = $extraInstantiationParams;
80 }
81
82 /**
83 * Destroys all contained service instances that implement the DestructibleService
84 * interface. This will render all services obtained from this MediaWikiServices
85 * instance unusable. In particular, this will disable access to the storage backend
86 * via any of these services. Any future call to getService() will throw an exception.
87 *
88 * @see resetGlobalInstance()
89 */
90 public function destroy() {
91 foreach ( $this->getServiceNames() as $name ) {
92 $service = $this->peekService( $name );
93 if ( $service !== null && $service instanceof DestructibleService ) {
94 $service->destroy();
95 }
96 }
97
98 $this->destroyed = true;
99 }
100
101 /**
102 * @param array $wiringFiles A list of PHP files to load wiring information from.
103 * Each file is loaded using PHP's include mechanism. Each file is expected to
104 * return an associative array that maps service names to instantiator functions.
105 */
106 public function loadWiringFiles( array $wiringFiles ) {
107 foreach ( $wiringFiles as $file ) {
108 // the wiring file is required to return an array of instantiators.
109 $wiring = require $file;
110
111 Assert::postcondition(
112 is_array( $wiring ),
113 "Wiring file $file is expected to return an array!"
114 );
115
116 $this->applyWiring( $wiring );
117 }
118 }
119
120 /**
121 * Registers multiple services (aka a "wiring").
122 *
123 * @param array $serviceInstantiators An associative array mapping service names to
124 * instantiator functions.
125 */
126 public function applyWiring( array $serviceInstantiators ) {
127 Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' );
128
129 foreach ( $serviceInstantiators as $name => $instantiator ) {
130 $this->defineService( $name, $instantiator );
131 }
132 }
133
134 /**
135 * Imports all wiring defined in $container. Wiring defined in $container
136 * will override any wiring already defined locally. However, already
137 * existing service instances will be preserved.
138 *
139 * @since 1.28
140 *
141 * @param ServiceContainer $container
142 * @param string[] $skip A list of service names to skip during import
143 */
144 public function importWiring( ServiceContainer $container, $skip = [] ) {
145 $newInstantiators = array_diff_key(
146 $container->serviceInstantiators,
147 array_flip( $skip )
148 );
149
150 $this->serviceInstantiators = array_merge(
151 $this->serviceInstantiators,
152 $newInstantiators
153 );
154 }
155
156 /**
157 * Returns true if a service is defined for $name, that is, if a call to getService( $name )
158 * would return a service instance.
159 *
160 * @param string $name
161 *
162 * @return bool
163 */
164 public function hasService( $name ) {
165 return isset( $this->serviceInstantiators[$name] );
166 }
167
168 /**
169 * Returns the service instance for $name only if that service has already been instantiated.
170 * This is intended for situations where services get destroyed/cleaned up, so we can
171 * avoid creating a service just to destroy it again.
172 *
173 * @note This is intended for internal use and for test fixtures.
174 * Application logic should use getService() instead.
175 *
176 * @see getService().
177 *
178 * @param string $name
179 *
180 * @return object|null The service instance, or null if the service has not yet been instantiated.
181 * @throws RuntimeException if $name does not refer to a known service.
182 */
183 public function peekService( $name ) {
184 if ( !$this->hasService( $name ) ) {
185 throw new NoSuchServiceException( $name );
186 }
187
188 return isset( $this->services[$name] ) ? $this->services[$name] : null;
189 }
190
191 /**
192 * @return string[]
193 */
194 public function getServiceNames() {
195 return array_keys( $this->serviceInstantiators );
196 }
197
198 /**
199 * Define a new service. The service must not be known already.
200 *
201 * @see getService().
202 * @see replaceService().
203 *
204 * @param string $name The name of the service to register, for use with getService().
205 * @param callable $instantiator Callback that returns a service instance.
206 * Will be called with this MediaWikiServices instance as the only parameter.
207 * Any extra instantiation parameters provided to the constructor will be
208 * passed as subsequent parameters when invoking the instantiator.
209 *
210 * @throws RuntimeException if there is already a service registered as $name.
211 */
212 public function defineService( $name, callable $instantiator ) {
213 Assert::parameterType( 'string', $name, '$name' );
214
215 if ( $this->hasService( $name ) ) {
216 throw new ServiceAlreadyDefinedException( $name );
217 }
218
219 $this->serviceInstantiators[$name] = $instantiator;
220 }
221
222 /**
223 * Replace an already defined service.
224 *
225 * @see defineService().
226 *
227 * @note This causes any previously instantiated instance of the service to be discarded.
228 *
229 * @param string $name The name of the service to register.
230 * @param callable $instantiator Callback function that returns a service instance.
231 * Will be called with this MediaWikiServices instance as the only parameter.
232 * The instantiator must return a service compatible with the originally defined service.
233 * Any extra instantiation parameters provided to the constructor will be
234 * passed as subsequent parameters when invoking the instantiator.
235 *
236 * @throws RuntimeException if $name is not a known service.
237 */
238 public function redefineService( $name, callable $instantiator ) {
239 Assert::parameterType( 'string', $name, '$name' );
240
241 if ( !$this->hasService( $name ) ) {
242 throw new NoSuchServiceException( $name );
243 }
244
245 if ( isset( $this->services[$name] ) ) {
246 throw new CannotReplaceActiveServiceException( $name );
247 }
248
249 $this->serviceInstantiators[$name] = $instantiator;
250 unset( $this->disabled[$name] );
251 }
252
253 /**
254 * Disables a service.
255 *
256 * @note Attempts to call getService() for a disabled service will result
257 * in a DisabledServiceException. Calling peekService for a disabled service will
258 * return null. Disabled services are listed by getServiceNames(). A disabled service
259 * can be enabled again using redefineService().
260 *
261 * @note If the service was already active (that is, instantiated) when getting disabled,
262 * and the service instance implements DestructibleService, destroy() is called on the
263 * service instance.
264 *
265 * @see redefineService()
266 * @see resetService()
267 *
268 * @param string $name The name of the service to disable.
269 *
270 * @throws RuntimeException if $name is not a known service.
271 */
272 public function disableService( $name ) {
273 $this->resetService( $name );
274
275 $this->disabled[$name] = true;
276 }
277
278 /**
279 * Resets a service by dropping the service instance.
280 * If the service instances implements DestructibleService, destroy()
281 * is called on the service instance.
282 *
283 * @warning This is generally unsafe! Other services may still retain references
284 * to the stale service instance, leading to failures and inconsistencies. Subclasses
285 * may use this method to reset specific services under specific instances, but
286 * it should not be exposed to application logic.
287 *
288 * @note This is declared final so subclasses can not interfere with the expectations
289 * disableService() has when calling resetService().
290 *
291 * @see redefineService()
292 * @see disableService().
293 *
294 * @param string $name The name of the service to reset.
295 * @param bool $destroy Whether the service instance should be destroyed if it exists.
296 * When set to false, any existing service instance will effectively be detached
297 * from the container.
298 *
299 * @throws RuntimeException if $name is not a known service.
300 */
301 final protected function resetService( $name, $destroy = true ) {
302 Assert::parameterType( 'string', $name, '$name' );
303
304 $instance = $this->peekService( $name );
305
306 if ( $destroy && $instance instanceof DestructibleService ) {
307 $instance->destroy();
308 }
309
310 unset( $this->services[$name] );
311 unset( $this->disabled[$name] );
312 }
313
314 /**
315 * Returns a service object of the kind associated with $name.
316 * Services instances are instantiated lazily, on demand.
317 * This method may or may not return the same service instance
318 * when called multiple times with the same $name.
319 *
320 * @note Rather than calling this method directly, it is recommended to provide
321 * getters with more meaningful names and more specific return types, using
322 * a subclass or wrapper.
323 *
324 * @see redefineService().
325 *
326 * @param string $name The service name
327 *
328 * @throws NoSuchServiceException if $name is not a known service.
329 * @throws ContainerDisabledException if this container has already been destroyed.
330 * @throws ServiceDisabledException if the requested service has been disabled.
331 *
332 * @return object The service instance
333 */
334 public function getService( $name ) {
335 if ( $this->destroyed ) {
336 throw new ContainerDisabledException();
337 }
338
339 if ( isset( $this->disabled[$name] ) ) {
340 throw new ServiceDisabledException( $name );
341 }
342
343 if ( !isset( $this->services[$name] ) ) {
344 $this->services[$name] = $this->createService( $name );
345 }
346
347 return $this->services[$name];
348 }
349
350 /**
351 * @param string $name
352 *
353 * @throws InvalidArgumentException if $name is not a known service.
354 * @return object
355 */
356 private function createService( $name ) {
357 if ( isset( $this->serviceInstantiators[$name] ) ) {
358 $service = call_user_func_array(
359 $this->serviceInstantiators[$name],
360 array_merge( [ $this ], $this->extraInstantiationParams )
361 );
362 // NOTE: when adding more wiring logic here, make sure copyWiring() is kept in sync!
363 } else {
364 throw new NoSuchServiceException( $name );
365 }
366
367 return $service;
368 }
369
370 /**
371 * @param string $name
372 * @return bool Whether the service is disabled
373 * @since 1.28
374 */
375 public function isServiceDisabled( $name ) {
376 return isset( $this->disabled[$name] );
377 }
378 }