MediaWiki REL1_34
ServiceContainer.php
Go to the documentation of this file.
1<?php
2
3namespace Wikimedia\Services;
4
5use InvalidArgumentException;
6use Psr\Container\ContainerInterface;
7use RuntimeException;
8use Wikimedia\Assert\Assert;
9use Wikimedia\ScopedCallback;
10
49class ServiceContainer implements ContainerInterface, DestructibleService {
50
54 private $services = [];
55
60
65
69 private $disabled = [];
70
75
79 private $destroyed = false;
80
85
91 public function __construct( array $extraInstantiationParams = [] ) {
92 $this->extraInstantiationParams = $extraInstantiationParams;
93 }
94
103 public function destroy() {
104 foreach ( $this->getServiceNames() as $name ) {
105 $service = $this->peekService( $name );
106 if ( $service !== null && $service instanceof DestructibleService ) {
107 $service->destroy();
108 }
109 }
110
111 // Break circular references due to the $this reference in closures, by
112 // erasing the instantiator array. This allows the ServiceContainer to
113 // be deleted when it goes out of scope.
114 $this->serviceInstantiators = [];
115 // Also remove the services themselves, to avoid confusion.
116 $this->services = [];
117 $this->destroyed = true;
118 }
119
125 public function loadWiringFiles( array $wiringFiles ) {
126 foreach ( $wiringFiles as $file ) {
127 // the wiring file is required to return an array of instantiators.
128 $wiring = require $file;
129
130 Assert::postcondition(
131 is_array( $wiring ),
132 "Wiring file $file is expected to return an array!"
133 );
134
135 $this->applyWiring( $wiring );
136 }
137 }
138
145 public function applyWiring( array $serviceInstantiators ) {
146 Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' );
147
148 foreach ( $serviceInstantiators as $name => $instantiator ) {
149 $this->defineService( $name, $instantiator );
150 }
151 }
152
163 public function importWiring( ServiceContainer $container, $skip = [] ) {
164 $newInstantiators = array_diff_key(
165 $container->serviceInstantiators,
166 array_flip( $skip )
167 );
168
169 $this->serviceInstantiators = array_merge(
170 $this->serviceInstantiators,
171 $newInstantiators
172 );
173
174 $newManipulators = array_diff(
175 array_keys( $container->serviceManipulators ),
176 $skip
177 );
178
179 foreach ( $newManipulators as $name ) {
180 if ( isset( $this->serviceManipulators[$name] ) ) {
181 $this->serviceManipulators[$name] = array_merge(
182 $this->serviceManipulators[$name],
183 $container->serviceManipulators[$name]
184 );
185 } else {
186 $this->serviceManipulators[$name] = $container->serviceManipulators[$name];
187 }
188 }
189 }
190
199 public function hasService( $name ) {
200 return isset( $this->serviceInstantiators[$name] );
201 }
202
204 public function has( $name ) {
205 return $this->hasService( $name );
206 }
207
223 public function peekService( $name ) {
224 if ( !$this->hasService( $name ) ) {
225 throw new NoSuchServiceException( $name );
226 }
227
228 return $this->services[$name] ?? null;
229 }
230
234 public function getServiceNames() {
235 return array_keys( $this->serviceInstantiators );
236 }
237
252 public function defineService( $name, callable $instantiator ) {
253 Assert::parameterType( 'string', $name, '$name' );
254
255 if ( $this->hasService( $name ) ) {
256 throw new ServiceAlreadyDefinedException( $name );
257 }
258
259 $this->serviceInstantiators[$name] = $instantiator;
260 }
261
281 public function redefineService( $name, callable $instantiator ) {
282 Assert::parameterType( 'string', $name, '$name' );
283
284 if ( !$this->hasService( $name ) ) {
285 throw new NoSuchServiceException( $name );
286 }
287
288 if ( isset( $this->services[$name] ) ) {
289 throw new CannotReplaceActiveServiceException( $name );
290 }
291
292 $this->serviceInstantiators[$name] = $instantiator;
293 unset( $this->disabled[$name] );
294 }
295
322 public function addServiceManipulator( $name, callable $manipulator ) {
323 Assert::parameterType( 'string', $name, '$name' );
324
325 if ( !$this->hasService( $name ) ) {
326 throw new NoSuchServiceException( $name );
327 }
328
329 if ( isset( $this->services[$name] ) ) {
330 throw new CannotReplaceActiveServiceException( $name );
331 }
332
333 $this->serviceManipulators[$name][] = $manipulator;
334 }
335
355 public function disableService( $name ) {
356 $this->resetService( $name );
357
358 $this->disabled[$name] = true;
359 }
360
384 final protected function resetService( $name, $destroy = true ) {
385 Assert::parameterType( 'string', $name, '$name' );
386
387 $instance = $this->peekService( $name );
388
389 if ( $destroy && $instance instanceof DestructibleService ) {
390 $instance->destroy();
391 }
392
393 unset( $this->services[$name] );
394 unset( $this->disabled[$name] );
395 }
396
417 public function getService( $name ) {
418 if ( $this->destroyed ) {
419 throw new ContainerDisabledException();
420 }
421
422 if ( isset( $this->disabled[$name] ) ) {
423 throw new ServiceDisabledException( $name );
424 }
425
426 if ( !isset( $this->services[$name] ) ) {
427 $this->services[$name] = $this->createService( $name );
428 }
429
430 return $this->services[$name];
431 }
432
434 public function get( $name ) {
435 return $this->getService( $name );
436 }
437
445 private function createService( $name ) {
446 if ( isset( $this->serviceInstantiators[$name] ) ) {
447 if ( isset( $this->servicesBeingCreated[$name] ) ) {
448 throw new RuntimeException( "Circular dependency when creating service! " .
449 implode( ' -> ', array_keys( $this->servicesBeingCreated ) ) . " -> $name" );
450 }
451 $this->servicesBeingCreated[$name] = true;
452 $removeFromStack = new ScopedCallback( function () use ( $name ) {
453 unset( $this->servicesBeingCreated[$name] );
454 } );
455
456 $service = ( $this->serviceInstantiators[$name] )(
457 $this,
458 ...$this->extraInstantiationParams
459 );
460
461 if ( isset( $this->serviceManipulators[$name] ) ) {
462 foreach ( $this->serviceManipulators[$name] as $callback ) {
463 $ret = call_user_func_array(
464 $callback,
465 array_merge( [ $service, $this ], $this->extraInstantiationParams )
466 );
467
468 // If the manipulator callback returns an object, that object replaces
469 // the original service instance. This allows the manipulator to wrap
470 // or fully replace the service.
471 if ( $ret !== null ) {
472 $service = $ret;
473 }
474 }
475 }
476
477 $removeFromStack->consume();
478
479 // NOTE: when adding more wiring logic here, make sure importWiring() is kept in sync!
480 } else {
481 throw new NoSuchServiceException( $name );
482 }
483
484 return $service;
485 }
486
492 public function isServiceDisabled( $name ) {
493 return isset( $this->disabled[$name] );
494 }
495}
496
501class_alias( ServiceContainer::class, 'MediaWiki\Services\ServiceContainer' );
Exception thrown when trying to replace an already active service.
Exception thrown when trying to access a service on a disabled container or factory.
Exception thrown when the requested service is not known.
Exception thrown when a service was already defined, but the caller expected it to not exist.
ServiceContainer provides a generic service to manage named services using lazy instantiation based o...
getService( $name)
Returns a service object of the kind associated with $name.
addServiceManipulator( $name, callable $manipulator)
Add a service manipulator callback for the given service.
resetService( $name, $destroy=true)
Resets a service by dropping the service instance.
__construct(array $extraInstantiationParams=[])
applyWiring(array $serviceInstantiators)
Registers multiple services (aka a "wiring").
importWiring(ServiceContainer $container, $skip=[])
Imports all wiring defined in $container.
defineService( $name, callable $instantiator)
Define a new service.
redefineService( $name, callable $instantiator)
Replace an already defined service.
destroy()
Destroys all contained service instances that implement the DestructibleService interface.
disableService( $name)
Disables a service.
peekService( $name)
Returns the service instance for $name only if that service has already been instantiated.
bool[] $disabled
disabled status, per service name
hasService( $name)
Returns true if a service is defined for $name, that is, if a call to getService( $name ) would retur...
array $servicesBeingCreated
Set of services currently being created, to detect loops.
Exception thrown when trying to access a disabled service.
DestructibleService defines a standard interface for shutting down a service instance.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42