3 namespace Wikimedia\Services
;
5 use InvalidArgumentException
;
6 use Psr\Container\ContainerInterface
;
8 use Wikimedia\Assert\Assert
;
11 * Generic service container.
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.
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.
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
34 * ServiceContainer provides a generic service to manage named services using
35 * lazy instantiation based on instantiator callback functions.
37 * Services managed by an instance of ServiceContainer may or may not implement
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
45 * @see docs/injection.txt for an overview of using dependency injection in the
46 * MediaWiki code base.
48 class ServiceContainer
implements ContainerInterface
, DestructibleService
{
53 private $services = [];
58 private $serviceInstantiators = [];
63 private $serviceManipulators = [];
66 * @var bool[] disabled status, per service name
68 private $disabled = [];
73 private $extraInstantiationParams;
78 private $destroyed = false;
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.
85 public function __construct( array $extraInstantiationParams = [] ) {
86 $this->extraInstantiationParams
= $extraInstantiationParams;
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.
95 * @see resetGlobalInstance()
97 public function destroy() {
98 foreach ( $this->getServiceNames() as $name ) {
99 $service = $this->peekService( $name );
100 if ( $service !== null && $service instanceof DestructibleService
) {
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;
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.
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;
124 Assert
::postcondition(
126 "Wiring file $file is expected to return an array!"
129 $this->applyWiring( $wiring );
134 * Registers multiple services (aka a "wiring").
136 * @param array $serviceInstantiators An associative array mapping service names to
137 * instantiator functions.
139 public function applyWiring( array $serviceInstantiators ) {
140 Assert
::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' );
142 foreach ( $serviceInstantiators as $name => $instantiator ) {
143 $this->defineService( $name, $instantiator );
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.
154 * @param ServiceContainer $container
155 * @param string[] $skip A list of service names to skip during import
157 public function importWiring( ServiceContainer
$container, $skip = [] ) {
158 $newInstantiators = array_diff_key(
159 $container->serviceInstantiators
,
163 $this->serviceInstantiators
= array_merge(
164 $this->serviceInstantiators
,
168 $newManipulators = array_diff(
169 array_keys( $container->serviceManipulators
),
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]
180 $this->serviceManipulators
[$name] = $container->serviceManipulators
[$name];
186 * Returns true if a service is defined for $name, that is, if a call to getService( $name )
187 * would return a service instance.
189 * @param string $name
193 public function hasService( $name ) {
194 return isset( $this->serviceInstantiators
[$name] );
198 public function has( $name ) {
199 return $this->hasService( $name );
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.
207 * @note This is intended for internal use and for test fixtures.
208 * Application logic should use getService() instead.
212 * @param string $name
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.
217 public function peekService( $name ) {
218 if ( !$this->hasService( $name ) ) {
219 throw new NoSuchServiceException( $name );
222 return $this->services
[$name] ??
null;
228 public function getServiceNames() {
229 return array_keys( $this->serviceInstantiators
);
233 * Define a new service. The service must not be known already.
236 * @see redefineService().
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.
244 * @throws RuntimeException if there is already a service registered as $name.
246 public function defineService( $name, callable
$instantiator ) {
247 Assert
::parameterType( 'string', $name, '$name' );
249 if ( $this->hasService( $name ) ) {
250 throw new ServiceAlreadyDefinedException( $name );
253 $this->serviceInstantiators
[$name] = $instantiator;
257 * Replace an already defined service.
259 * @see defineService().
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.
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.
272 * @throws NoSuchServiceException if $name is not a known service.
273 * @throws CannotReplaceActiveServiceException if the service was already instantiated.
275 public function redefineService( $name, callable
$instantiator ) {
276 Assert
::parameterType( 'string', $name, '$name' );
278 if ( !$this->hasService( $name ) ) {
279 throw new NoSuchServiceException( $name );
282 if ( isset( $this->services
[$name] ) ) {
283 throw new CannotReplaceActiveServiceException( $name );
286 $this->serviceInstantiators
[$name] = $instantiator;
287 unset( $this->disabled
[$name] );
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.
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
299 * @see defineService().
300 * @see redefineService().
302 * @note This will fail if the service was already instantiated.
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.
313 * @throws NoSuchServiceException if $name is not a known service.
314 * @throws CannotReplaceActiveServiceException if the service was already instantiated.
316 public function addServiceManipulator( $name, callable
$manipulator ) {
317 Assert
::parameterType( 'string', $name, '$name' );
319 if ( !$this->hasService( $name ) ) {
320 throw new NoSuchServiceException( $name );
323 if ( isset( $this->services
[$name] ) ) {
324 throw new CannotReplaceActiveServiceException( $name );
327 $this->serviceManipulators
[$name][] = $manipulator;
331 * Disables a service.
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().
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
342 * @see redefineService()
343 * @see resetService()
345 * @param string $name The name of the service to disable.
347 * @throws RuntimeException if $name is not a known service.
349 public function disableService( $name ) {
350 $this->resetService( $name );
352 $this->disabled
[$name] = true;
356 * Resets a service by dropping the service instance.
357 * If the service instances implements DestructibleService, destroy()
358 * is called on the service instance.
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.
365 * @note This is declared final so subclasses can not interfere with the expectations
366 * disableService() has when calling resetService().
368 * @see redefineService()
369 * @see disableService().
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.
376 * @throws RuntimeException if $name is not a known service.
378 final protected function resetService( $name, $destroy = true ) {
379 Assert
::parameterType( 'string', $name, '$name' );
381 $instance = $this->peekService( $name );
383 if ( $destroy && $instance instanceof DestructibleService
) {
384 $instance->destroy();
387 unset( $this->services
[$name] );
388 unset( $this->disabled
[$name] );
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.
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.
401 * @see redefineService().
403 * @param string $name The service name
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.
409 * @return mixed The service instance
411 public function getService( $name ) {
412 if ( $this->destroyed
) {
413 throw new ContainerDisabledException();
416 if ( isset( $this->disabled
[$name] ) ) {
417 throw new ServiceDisabledException( $name );
420 if ( !isset( $this->services
[$name] ) ) {
421 $this->services
[$name] = $this->createService( $name );
424 return $this->services
[$name];
428 public function get( $name ) {
429 return $this->getService( $name );
433 * @param string $name
435 * @throws InvalidArgumentException if $name is not a known service.
438 private function createService( $name ) {
439 if ( isset( $this->serviceInstantiators
[$name] ) ) {
440 $service = ( $this->serviceInstantiators
[$name] )(
442 ...$this->extraInstantiationParams
445 if ( isset( $this->serviceManipulators
[$name] ) ) {
446 foreach ( $this->serviceManipulators
[$name] as $callback ) {
447 $ret = call_user_func_array(
449 array_merge( [ $service, $this ], $this->extraInstantiationParams
)
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 ) {
461 // NOTE: when adding more wiring logic here, make sure importWiring() is kept in sync!
463 throw new NoSuchServiceException( $name );
470 * @param string $name
471 * @return bool Whether the service is disabled
474 public function isServiceDisabled( $name ) {
475 return isset( $this->disabled
[$name] );
480 * Retain the old class name for backwards compatibility.
481 * @deprecated since 1.33
483 class_alias( ServiceContainer
::class, 'MediaWiki\Services\ServiceContainer' );