Merge "Print styles: Wrap CSS-generated URLs"
[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 array
60 */
61 private $extraInstantiationParams;
62
63 /**
64 * @var boolean
65 */
66 private $destroyed = false;
67
68 /**
69 * @param array $extraInstantiationParams Any additional parameters to be passed to the
70 * instantiator function when creating a service. This is typically used to provide
71 * access to additional ServiceContainers or Config objects.
72 */
73 public function __construct( array $extraInstantiationParams = [] ) {
74 $this->extraInstantiationParams = $extraInstantiationParams;
75 }
76
77 /**
78 * Destroys all contained service instances that implement the DestructibleService
79 * interface. This will render all services obtained from this MediaWikiServices
80 * instance unusable. In particular, this will disable access to the storage backend
81 * via any of these services. Any future call to getService() will throw an exception.
82 *
83 * @see resetGlobalInstance()
84 */
85 public function destroy() {
86 foreach ( $this->getServiceNames() as $name ) {
87 $service = $this->peekService( $name );
88 if ( $service !== null && $service instanceof DestructibleService ) {
89 $service->destroy();
90 }
91 }
92
93 $this->destroyed = true;
94 }
95
96 /**
97 * @param array $wiringFiles A list of PHP files to load wiring information from.
98 * Each file is loaded using PHP's include mechanism. Each file is expected to
99 * return an associative array that maps service names to instantiator functions.
100 */
101 public function loadWiringFiles( array $wiringFiles ) {
102 foreach ( $wiringFiles as $file ) {
103 // the wiring file is required to return an array of instantiators.
104 $wiring = require $file;
105
106 Assert::postcondition(
107 is_array( $wiring ),
108 "Wiring file $file is expected to return an array!"
109 );
110
111 $this->applyWiring( $wiring );
112 }
113 }
114
115 /**
116 * Registers multiple services (aka a "wiring").
117 *
118 * @param array $serviceInstantiators An associative array mapping service names to
119 * instantiator functions.
120 */
121 public function applyWiring( array $serviceInstantiators ) {
122 Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' );
123
124 foreach ( $serviceInstantiators as $name => $instantiator ) {
125 $this->defineService( $name, $instantiator );
126 }
127 }
128
129 /**
130 * Returns true if a service is defined for $name, that is, if a call to getService( $name )
131 * would return a service instance.
132 *
133 * @param string $name
134 *
135 * @return bool
136 */
137 public function hasService( $name ) {
138 return isset( $this->serviceInstantiators[$name] );
139 }
140
141 /**
142 * Returns the service instance for $name only if that service has already been instantiated.
143 * This is intended for situations where services get destroyed/cleaned up, so we can
144 * avoid creating a service just to destroy it again.
145 *
146 * @note This is intended for internal use and for test fixtures.
147 * Application logic should use getService() instead.
148 *
149 * @see getService().
150 *
151 * @param string $name
152 *
153 * @return object|null The service instance, or null if the service has not yet been instantiated.
154 * @throws RuntimeException if $name does not refer to a known service.
155 */
156 public function peekService( $name ) {
157 if ( !$this->hasService( $name ) ) {
158 throw new NoSuchServiceException( $name );
159 }
160
161 return isset( $this->services[$name] ) ? $this->services[$name] : null;
162 }
163
164 /**
165 * @return string[]
166 */
167 public function getServiceNames() {
168 return array_keys( $this->serviceInstantiators );
169 }
170
171 /**
172 * Define a new service. The service must not be known already.
173 *
174 * @see getService().
175 * @see replaceService().
176 *
177 * @param string $name The name of the service to register, for use with getService().
178 * @param callable $instantiator Callback that returns a service instance.
179 * Will be called with this MediaWikiServices instance as the only parameter.
180 * Any extra instantiation parameters provided to the constructor will be
181 * passed as subsequent parameters when invoking the instantiator.
182 *
183 * @throws RuntimeException if there is already a service registered as $name.
184 */
185 public function defineService( $name, callable $instantiator ) {
186 Assert::parameterType( 'string', $name, '$name' );
187
188 if ( $this->hasService( $name ) ) {
189 throw new ServiceAlreadyDefinedException( $name );
190 }
191
192 $this->serviceInstantiators[$name] = $instantiator;
193 }
194
195 /**
196 * Replace an already defined service.
197 *
198 * @see defineService().
199 *
200 * @note This causes any previously instantiated instance of the service to be discarded.
201 *
202 * @param string $name The name of the service to register.
203 * @param callable $instantiator Callback function that returns a service instance.
204 * Will be called with this MediaWikiServices instance as the only parameter.
205 * The instantiator must return a service compatible with the originally defined service.
206 * Any extra instantiation parameters provided to the constructor will be
207 * passed as subsequent parameters when invoking the instantiator.
208 *
209 * @throws RuntimeException if $name is not a known service.
210 */
211 public function redefineService( $name, callable $instantiator ) {
212 Assert::parameterType( 'string', $name, '$name' );
213
214 if ( !$this->hasService( $name ) ) {
215 throw new NoSuchServiceException( $name );
216 }
217
218 if ( isset( $this->services[$name] ) ) {
219 throw new CannotReplaceActiveServiceException( $name );
220 }
221
222 $this->serviceInstantiators[$name] = $instantiator;
223 }
224
225 /**
226 * Disables a service.
227 *
228 * @note Attempts to call getService() for a disabled service will result
229 * in a DisabledServiceException. Calling peekService for a disabled service will
230 * return null. Disabled services are listed by getServiceNames(). A disabled service
231 * can be enabled again using redefineService().
232 *
233 * @note If the service was already active (that is, instantiated) when getting disabled,
234 * and the service instance implements DestructibleService, destroy() is called on the
235 * service instance.
236 *
237 * @see redefineService()
238 * @see resetService()
239 *
240 * @param string $name The name of the service to disable.
241 *
242 * @throws RuntimeException if $name is not a known service.
243 */
244 public function disableService( $name ) {
245 $this->resetService( $name );
246
247 $this->redefineService( $name, function() use ( $name ) {
248 throw new ServiceDisabledException( $name );
249 } );
250 }
251
252 /**
253 * Resets a service by dropping the service instance.
254 * If the service instances implements DestructibleService, destroy()
255 * is called on the service instance.
256 *
257 * @warning This is generally unsafe! Other services may still retain references
258 * to the stale service instance, leading to failures and inconsistencies. Subclasses
259 * may use this method to reset specific services under specific instances, but
260 * it should not be exposed to application logic.
261 *
262 * @note This is declared final so subclasses can not interfere with the expectations
263 * disableService() has when calling resetService().
264 *
265 * @see redefineService()
266 * @see disableService().
267 *
268 * @param string $name The name of the service to reset.
269 * @param bool $destroy Whether the service instance should be destroyed if it exists.
270 * When set to false, any existing service instance will effectively be detached
271 * from the container.
272 *
273 * @throws RuntimeException if $name is not a known service.
274 */
275 final protected function resetService( $name, $destroy = true ) {
276 Assert::parameterType( 'string', $name, '$name' );
277
278 $instance = $this->peekService( $name );
279
280 if ( $destroy && $instance instanceof DestructibleService ) {
281 $instance->destroy();
282 }
283
284 unset( $this->services[$name] );
285 }
286
287 /**
288 * Returns a service object of the kind associated with $name.
289 * Services instances are instantiated lazily, on demand.
290 * This method may or may not return the same service instance
291 * when called multiple times with the same $name.
292 *
293 * @note Rather than calling this method directly, it is recommended to provide
294 * getters with more meaningful names and more specific return types, using
295 * a subclass or wrapper.
296 *
297 * @see redefineService().
298 *
299 * @param string $name The service name
300 *
301 * @throws NoSuchServiceException if $name is not a known service.
302 * @throws ServiceDisabledException if this container has already been destroyed.
303 *
304 * @return object The service instance
305 */
306 public function getService( $name ) {
307 if ( $this->destroyed ) {
308 throw new ContainerDisabledException();
309 }
310
311 if ( !isset( $this->services[$name] ) ) {
312 $this->services[$name] = $this->createService( $name );
313 }
314
315 return $this->services[$name];
316 }
317
318 /**
319 * @param string $name
320 *
321 * @throws InvalidArgumentException if $name is not a known service.
322 * @return object
323 */
324 private function createService( $name ) {
325 if ( isset( $this->serviceInstantiators[$name] ) ) {
326 $service = call_user_func_array(
327 $this->serviceInstantiators[$name],
328 array_merge( [ $this ], $this->extraInstantiationParams )
329 );
330 } else {
331 throw new NoSuchServiceException( $name );
332 }
333
334 return $service;
335 }
336
337 }