Completed
Branch master (d58858)
by
unknown
28:23
created

ServiceContainer::createService()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 12
c 1
b 0
f 0
rs 9.4285
cc 2
eloc 8
nc 2
nop 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 {
47
48
	/**
49
	 * @var object[]
50
	 */
51
	private $services = [];
52
53
	/**
54
	 * @var callable[]
55
	 */
56
	private $serviceInstantiators = [];
57
58
	/**
59
	 * @var array
60
	 */
61
	private $extraInstantiationParams;
62
63
	/**
64
	 * @param array $extraInstantiationParams Any additional parameters to be passed to the
65
	 * instantiator function when creating a service. This is typically used to provide
66
	 * access to additional ServiceContainers or Config objects.
67
	 */
68
	public function __construct( array $extraInstantiationParams = [] ) {
69
		$this->extraInstantiationParams = $extraInstantiationParams;
70
	}
71
72
	/**
73
	 * @param array $wiringFiles A list of PHP files to load wiring information from.
74
	 * Each file is loaded using PHP's include mechanism. Each file is expected to
75
	 * return an associative array that maps service names to instantiator functions.
76
	 */
77
	public function loadWiringFiles( array $wiringFiles ) {
78
		foreach ( $wiringFiles as $file ) {
79
			// the wiring file is required to return an array of instantiators.
80
			$wiring = require $file;
81
82
			Assert::postcondition(
83
				is_array( $wiring ),
84
				"Wiring file $file is expected to return an array!"
85
			);
86
87
			$this->applyWiring( $wiring );
88
		}
89
	}
90
91
	/**
92
	 * Registers multiple services (aka a "wiring").
93
	 *
94
	 * @param array $serviceInstantiators An associative array mapping service names to
95
	 *        instantiator functions.
96
	 */
97
	public function applyWiring( array $serviceInstantiators ) {
98
		Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' );
99
100
		foreach ( $serviceInstantiators as $name => $instantiator ) {
101
			$this->defineService( $name, $instantiator );
102
		}
103
	}
104
105
	/**
106
	 * Returns true if a service is defined for $name, that is, if a call to getService( $name )
107
	 * would return a service instance.
108
	 *
109
	 * @param string $name
110
	 *
111
	 * @return bool
112
	 */
113
	public function hasService( $name ) {
114
		return isset( $this->serviceInstantiators[$name] );
115
	}
116
117
	/**
118
	 * @return string[]
119
	 */
120
	public function getServiceNames() {
121
		return array_keys( $this->serviceInstantiators );
122
	}
123
124
	/**
125
	 * Define a new service. The service must not be known already.
126
	 *
127
	 * @see getService().
128
	 * @see replaceService().
129
	 *
130
	 * @param string $name The name of the service to register, for use with getService().
131
	 * @param callable $instantiator Callback that returns a service instance.
132
	 *        Will be called with this MediaWikiServices instance as the only parameter.
133
	 *        Any extra instantiation parameters provided to the constructor will be
134
	 *        passed as subsequent parameters when invoking the instantiator.
135
	 *
136
	 * @throws RuntimeException if there is already a service registered as $name.
137
	 */
138
	public function defineService( $name, callable $instantiator ) {
139
		Assert::parameterType( 'string', $name, '$name' );
140
141
		if ( $this->hasService( $name ) ) {
142
			throw new RuntimeException( 'Service already defined: ' . $name );
143
		}
144
145
		$this->serviceInstantiators[$name] = $instantiator;
146
	}
147
148
	/**
149
	 * Replace an already defined service.
150
	 *
151
	 * @see defineService().
152
	 *
153
	 * @note This causes any previously instantiated instance of the service to be discarded.
154
	 *
155
	 * @param string $name The name of the service to register.
156
	 * @param callable $instantiator Callback function that returns a service instance.
157
	 *        Will be called with this MediaWikiServices instance as the only parameter.
158
	 *        The instantiator must return a service compatible with the originally defined service.
159
	 *        Any extra instantiation parameters provided to the constructor will be
160
	 *        passed as subsequent parameters when invoking the instantiator.
161
	 *
162
	 * @throws RuntimeException if $name is not a known service.
163
	 */
164
	public function redefineService( $name, callable $instantiator ) {
165
		Assert::parameterType( 'string', $name, '$name' );
166
167
		if ( !$this->hasService( $name ) ) {
168
			throw new RuntimeException( 'Service not defined: ' . $name );
169
		}
170
171
		if ( isset( $this->services[$name] ) ) {
172
			throw new RuntimeException( 'Cannot redefine a service that is already in use: ' . $name );
173
		}
174
175
		$this->serviceInstantiators[$name] = $instantiator;
176
	}
177
178
	/**
179
	 * Returns a service object of the kind associated with $name.
180
	 * Services instances are instantiated lazily, on demand.
181
	 * This method may or may not return the same service instance
182
	 * when called multiple times with the same $name.
183
	 *
184
	 * @note Rather than calling this method directly, it is recommended to provide
185
	 * getters with more meaningful names and more specific return types, using
186
	 * a subclass or wrapper.
187
	 *
188
	 * @see redefineService().
189
	 *
190
	 * @param string $name The service name
191
	 *
192
	 * @throws InvalidArgumentException if $name is not a known service.
193
	 * @return object The service instance
194
	 */
195
	public function getService( $name ) {
196
		if ( !isset( $this->services[$name] ) ) {
197
			$this->services[$name] = $this->createService( $name );
198
		}
199
200
		return $this->services[$name];
201
	}
202
203
	/**
204
	 * @param string $name
205
	 *
206
	 * @throws InvalidArgumentException if $name is not a known service.
207
	 * @return object
208
	 */
209
	private function createService( $name ) {
210
		if ( isset( $this->serviceInstantiators[$name] ) ) {
211
			$service = call_user_func_array(
212
				$this->serviceInstantiators[$name],
213
				array_merge( [ $this ], $this->extraInstantiationParams )
214
			);
215
		} else {
216
			throw new InvalidArgumentException( 'Unknown service: ' . $name );
217
		}
218
219
		return $service;
220
	}
221
222
}
223