Completed
Branch master (467086)
by
unknown
30:56
created

ServiceContainer   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 333
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 333
rs 9.8
c 1
b 0
f 0
wmc 31
lcom 1
cbo 7

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A destroy() 0 10 4
A loadWiringFiles() 0 13 2
A applyWiring() 0 7 2
A importWiring() 0 11 1
A hasService() 0 3 1
A peekService() 0 7 3
A getServiceNames() 0 3 1
A defineService() 0 9 2
A redefineService() 0 14 3
A disableService() 0 5 1
A resetService() 0 12 3
A getService() 0 15 4
A createService() 0 13 2
A isServiceDisabled() 0 3 1
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
}
379