summaryrefslogtreecommitdiff
path: root/www/wiki/includes/services/ServiceContainer.php
blob: 9f09e22fc4d97cf189da5a1090d433be54ba75a6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
<?php
namespace MediaWiki\Services;

use InvalidArgumentException;
use RuntimeException;
use Wikimedia\Assert\Assert;

/**
 * Generic service container.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 *
 * @since 1.27
 */

/**
 * ServiceContainer provides a generic service to manage named services using
 * lazy instantiation based on instantiator callback functions.
 *
 * Services managed by an instance of ServiceContainer may or may not implement
 * a common interface.
 *
 * @note When using ServiceContainer to manage a set of services, consider
 * creating a wrapper or a subclass that provides access to the services via
 * getter methods with more meaningful names and more specific return type
 * declarations.
 *
 * @see docs/injection.txt for an overview of using dependency injection in the
 *      MediaWiki code base.
 */
class ServiceContainer implements DestructibleService {

	/**
	 * @var object[]
	 */
	private $services = [];

	/**
	 * @var callable[]
	 */
	private $serviceInstantiators = [];

	/**
	 * @var bool[] disabled status, per service name
	 */
	private $disabled = [];

	/**
	 * @var array
	 */
	private $extraInstantiationParams;

	/**
	 * @var bool
	 */
	private $destroyed = false;

	/**
	 * @param array $extraInstantiationParams Any additional parameters to be passed to the
	 * instantiator function when creating a service. This is typically used to provide
	 * access to additional ServiceContainers or Config objects.
	 */
	public function __construct( array $extraInstantiationParams = [] ) {
		$this->extraInstantiationParams = $extraInstantiationParams;
	}

	/**
	 * Destroys all contained service instances that implement the DestructibleService
	 * interface. This will render all services obtained from this MediaWikiServices
	 * instance unusable. In particular, this will disable access to the storage backend
	 * via any of these services. Any future call to getService() will throw an exception.
	 *
	 * @see resetGlobalInstance()
	 */
	public function destroy() {
		foreach ( $this->getServiceNames() as $name ) {
			$service = $this->peekService( $name );
			if ( $service !== null && $service instanceof DestructibleService ) {
				$service->destroy();
			}
		}

		$this->destroyed = true;
	}

	/**
	 * @param array $wiringFiles A list of PHP files to load wiring information from.
	 * Each file is loaded using PHP's include mechanism. Each file is expected to
	 * return an associative array that maps service names to instantiator functions.
	 */
	public function loadWiringFiles( array $wiringFiles ) {
		foreach ( $wiringFiles as $file ) {
			// the wiring file is required to return an array of instantiators.
			$wiring = require $file;

			Assert::postcondition(
				is_array( $wiring ),
				"Wiring file $file is expected to return an array!"
			);

			$this->applyWiring( $wiring );
		}
	}

	/**
	 * Registers multiple services (aka a "wiring").
	 *
	 * @param array $serviceInstantiators An associative array mapping service names to
	 *        instantiator functions.
	 */
	public function applyWiring( array $serviceInstantiators ) {
		Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' );

		foreach ( $serviceInstantiators as $name => $instantiator ) {
			$this->defineService( $name, $instantiator );
		}
	}

	/**
	 * Imports all wiring defined in $container. Wiring defined in $container
	 * will override any wiring already defined locally. However, already
	 * existing service instances will be preserved.
	 *
	 * @since 1.28
	 *
	 * @param ServiceContainer $container
	 * @param string[] $skip A list of service names to skip during import
	 */
	public function importWiring( ServiceContainer $container, $skip = [] ) {
		$newInstantiators = array_diff_key(
			$container->serviceInstantiators,
			array_flip( $skip )
		);

		$this->serviceInstantiators = array_merge(
			$this->serviceInstantiators,
			$newInstantiators
		);
	}

	/**
	 * Returns true if a service is defined for $name, that is, if a call to getService( $name )
	 * would return a service instance.
	 *
	 * @param string $name
	 *
	 * @return bool
	 */
	public function hasService( $name ) {
		return isset( $this->serviceInstantiators[$name] );
	}

	/**
	 * Returns the service instance for $name only if that service has already been instantiated.
	 * This is intended for situations where services get destroyed/cleaned up, so we can
	 * avoid creating a service just to destroy it again.
	 *
	 * @note This is intended for internal use and for test fixtures.
	 * Application logic should use getService() instead.
	 *
	 * @see getService().
	 *
	 * @param string $name
	 *
	 * @return object|null The service instance, or null if the service has not yet been instantiated.
	 * @throws RuntimeException if $name does not refer to a known service.
	 */
	public function peekService( $name ) {
		if ( !$this->hasService( $name ) ) {
			throw new NoSuchServiceException( $name );
		}

		return isset( $this->services[$name] ) ? $this->services[$name] : null;
	}

	/**
	 * @return string[]
	 */
	public function getServiceNames() {
		return array_keys( $this->serviceInstantiators );
	}

	/**
	 * Define a new service. The service must not be known already.
	 *
	 * @see getService().
	 * @see replaceService().
	 *
	 * @param string $name The name of the service to register, for use with getService().
	 * @param callable $instantiator Callback that returns a service instance.
	 *        Will be called with this MediaWikiServices instance as the only parameter.
	 *        Any extra instantiation parameters provided to the constructor will be
	 *        passed as subsequent parameters when invoking the instantiator.
	 *
	 * @throws RuntimeException if there is already a service registered as $name.
	 */
	public function defineService( $name, callable $instantiator ) {
		Assert::parameterType( 'string', $name, '$name' );

		if ( $this->hasService( $name ) ) {
			throw new ServiceAlreadyDefinedException( $name );
		}

		$this->serviceInstantiators[$name] = $instantiator;
	}

	/**
	 * Replace an already defined service.
	 *
	 * @see defineService().
	 *
	 * @note This causes any previously instantiated instance of the service to be discarded.
	 *
	 * @param string $name The name of the service to register.
	 * @param callable $instantiator Callback function that returns a service instance.
	 *        Will be called with this MediaWikiServices instance as the only parameter.
	 *        The instantiator must return a service compatible with the originally defined service.
	 *        Any extra instantiation parameters provided to the constructor will be
	 *        passed as subsequent parameters when invoking the instantiator.
	 *
	 * @throws RuntimeException if $name is not a known service.
	 */
	public function redefineService( $name, callable $instantiator ) {
		Assert::parameterType( 'string', $name, '$name' );

		if ( !$this->hasService( $name ) ) {
			throw new NoSuchServiceException( $name );
		}

		if ( isset( $this->services[$name] ) ) {
			throw new CannotReplaceActiveServiceException( $name );
		}

		$this->serviceInstantiators[$name] = $instantiator;
		unset( $this->disabled[$name] );
	}

	/**
	 * Disables a service.
	 *
	 * @note Attempts to call getService() for a disabled service will result
	 * in a DisabledServiceException. Calling peekService for a disabled service will
	 * return null. Disabled services are listed by getServiceNames(). A disabled service
	 * can be enabled again using redefineService().
	 *
	 * @note If the service was already active (that is, instantiated) when getting disabled,
	 * and the service instance implements DestructibleService, destroy() is called on the
	 * service instance.
	 *
	 * @see redefineService()
	 * @see resetService()
	 *
	 * @param string $name The name of the service to disable.
	 *
	 * @throws RuntimeException if $name is not a known service.
	 */
	public function disableService( $name ) {
		$this->resetService( $name );

		$this->disabled[$name] = true;
	}

	/**
	 * Resets a service by dropping the service instance.
	 * If the service instances implements DestructibleService, destroy()
	 * is called on the service instance.
	 *
	 * @warning This is generally unsafe! Other services may still retain references
	 * to the stale service instance, leading to failures and inconsistencies. Subclasses
	 * may use this method to reset specific services under specific instances, but
	 * it should not be exposed to application logic.
	 *
	 * @note This is declared final so subclasses can not interfere with the expectations
	 * disableService() has when calling resetService().
	 *
	 * @see redefineService()
	 * @see disableService().
	 *
	 * @param string $name The name of the service to reset.
	 * @param bool $destroy Whether the service instance should be destroyed if it exists.
	 *        When set to false, any existing service instance will effectively be detached
	 *        from the container.
	 *
	 * @throws RuntimeException if $name is not a known service.
	 */
	final protected function resetService( $name, $destroy = true ) {
		Assert::parameterType( 'string', $name, '$name' );

		$instance = $this->peekService( $name );

		if ( $destroy && $instance instanceof DestructibleService ) {
			$instance->destroy();
		}

		unset( $this->services[$name] );
		unset( $this->disabled[$name] );
	}

	/**
	 * Returns a service object of the kind associated with $name.
	 * Services instances are instantiated lazily, on demand.
	 * This method may or may not return the same service instance
	 * when called multiple times with the same $name.
	 *
	 * @note Rather than calling this method directly, it is recommended to provide
	 * getters with more meaningful names and more specific return types, using
	 * a subclass or wrapper.
	 *
	 * @see redefineService().
	 *
	 * @param string $name The service name
	 *
	 * @throws NoSuchServiceException if $name is not a known service.
	 * @throws ContainerDisabledException if this container has already been destroyed.
	 * @throws ServiceDisabledException if the requested service has been disabled.
	 *
	 * @return object The service instance
	 */
	public function getService( $name ) {
		if ( $this->destroyed ) {
			throw new ContainerDisabledException();
		}

		if ( isset( $this->disabled[$name] ) ) {
			throw new ServiceDisabledException( $name );
		}

		if ( !isset( $this->services[$name] ) ) {
			$this->services[$name] = $this->createService( $name );
		}

		return $this->services[$name];
	}

	/**
	 * @param string $name
	 *
	 * @throws InvalidArgumentException if $name is not a known service.
	 * @return object
	 */
	private function createService( $name ) {
		if ( isset( $this->serviceInstantiators[$name] ) ) {
			$service = call_user_func_array(
				$this->serviceInstantiators[$name],
				array_merge( [ $this ], $this->extraInstantiationParams )
			);
			// NOTE: when adding more wiring logic here, make sure copyWiring() is kept in sync!
		} else {
			throw new NoSuchServiceException( $name );
		}

		return $service;
	}

	/**
	 * @param string $name
	 * @return bool Whether the service is disabled
	 * @since 1.28
	 */
	public function isServiceDisabled( $name ) {
		return isset( $this->disabled[$name] );
	}
}