MediaWiki  1.34.0
ServiceContainer.php
Go to the documentation of this file.
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 use Wikimedia\ScopedCallback;
10 
49 class ServiceContainer implements ContainerInterface, DestructibleService {
50 
54  private $services = [];
55 
59  private $serviceInstantiators = [];
60 
64  private $serviceManipulators = [];
65 
69  private $disabled = [];
70 
75 
79  private $destroyed = false;
80 
84  private $servicesBeingCreated = [];
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 
501 class_alias( ServiceContainer::class, 'MediaWiki\Services\ServiceContainer' );
Wikimedia\Services\ServiceContainer\$serviceManipulators
callable[][] $serviceManipulators
Definition: ServiceContainer.php:64
Wikimedia\Services\CannotReplaceActiveServiceException
Exception thrown when trying to replace an already active service.
Definition: CannotReplaceActiveServiceException.php:35
Wikimedia\Services\ServiceContainer\hasService
hasService( $name)
Returns true if a service is defined for $name, that is, if a call to getService( $name ) would retur...
Definition: ServiceContainer.php:199
Wikimedia\Services\ServiceContainer\has
has( $name)
Definition: ServiceContainer.php:204
Wikimedia\Services\ServiceContainer\$serviceInstantiators
callable[] $serviceInstantiators
Definition: ServiceContainer.php:59
Wikimedia\Services\ServiceContainer\isServiceDisabled
isServiceDisabled( $name)
Definition: ServiceContainer.php:492
Wikimedia\Services\ServiceContainer\__construct
__construct(array $extraInstantiationParams=[])
Definition: ServiceContainer.php:91
Wikimedia\Services\ServiceContainer\getServiceNames
getServiceNames()
Definition: ServiceContainer.php:234
Wikimedia\Services\ServiceContainer\destroy
destroy()
Destroys all contained service instances that implement the DestructibleService interface.
Definition: ServiceContainer.php:103
Wikimedia\Services\ServiceAlreadyDefinedException
Exception thrown when a service was already defined, but the caller expected it to not exist.
Definition: ServiceAlreadyDefinedException.php:37
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
Wikimedia\Services\ServiceContainer\redefineService
redefineService( $name, callable $instantiator)
Replace an already defined service.
Definition: ServiceContainer.php:281
Wikimedia\Services\ServiceContainer\loadWiringFiles
loadWiringFiles(array $wiringFiles)
Definition: ServiceContainer.php:125
Wikimedia\Services\NoSuchServiceException
Exception thrown when the requested service is not known.
Definition: NoSuchServiceException.php:35
Wikimedia\Services\ServiceContainer\$disabled
bool[] $disabled
disabled status, per service name
Definition: ServiceContainer.php:69
Wikimedia\Services\ServiceContainer\$servicesBeingCreated
array $servicesBeingCreated
Set of services currently being created, to detect loops.
Definition: ServiceContainer.php:84
Wikimedia\Services\ServiceContainer
ServiceContainer provides a generic service to manage named services using lazy instantiation based o...
Definition: ServiceContainer.php:49
Wikimedia\Services\DestructibleService
DestructibleService defines a standard interface for shutting down a service instance.
Definition: DestructibleService.php:36
Wikimedia\Services\ServiceContainer\applyWiring
applyWiring(array $serviceInstantiators)
Registers multiple services (aka a "wiring").
Definition: ServiceContainer.php:145
Wikimedia\Services\ServiceContainer\peekService
peekService( $name)
Returns the service instance for $name only if that service has already been instantiated.
Definition: ServiceContainer.php:223
Wikimedia\Services\ServiceContainer\resetService
resetService( $name, $destroy=true)
Resets a service by dropping the service instance.
Definition: ServiceContainer.php:384
Wikimedia\Services\ServiceContainer\defineService
defineService( $name, callable $instantiator)
Define a new service.
Definition: ServiceContainer.php:252
Wikimedia\Services\ServiceContainer\createService
createService( $name)
Definition: ServiceContainer.php:445
Wikimedia\Services\ServiceDisabledException
Exception thrown when trying to access a disabled service.
Definition: ServiceDisabledException.php:35
Wikimedia\Services\ServiceContainer\$services
object[] $services
Definition: ServiceContainer.php:54
Wikimedia\Services\ServiceContainer\importWiring
importWiring(ServiceContainer $container, $skip=[])
Imports all wiring defined in $container.
Definition: ServiceContainer.php:163
Wikimedia\Services\ServiceContainer\disableService
disableService( $name)
Disables a service.
Definition: ServiceContainer.php:355
Wikimedia\Services\ContainerDisabledException
Exception thrown when trying to access a service on a disabled container or factory.
Definition: ContainerDisabledException.php:35
Wikimedia\Services\ServiceContainer\$destroyed
bool $destroyed
Definition: ServiceContainer.php:79
Wikimedia\Services
Definition: CannotReplaceActiveServiceException.php:3
Wikimedia\Services\ServiceContainer\getService
getService( $name)
Returns a service object of the kind associated with $name.
Definition: ServiceContainer.php:417
Wikimedia\Services\ServiceContainer\$extraInstantiationParams
array $extraInstantiationParams
Definition: ServiceContainer.php:74
Wikimedia\Services\ServiceContainer\addServiceManipulator
addServiceManipulator( $name, callable $manipulator)
Add a service manipulator callback for the given service.
Definition: ServiceContainer.php:322