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