MediaWiki  master
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( string $name, callable $instantiator ) {
253  if ( $this->hasService( $name ) ) {
254  throw new ServiceAlreadyDefinedException( $name );
255  }
256 
257  $this->serviceInstantiators[$name] = $instantiator;
258  }
259 
279  public function redefineService( string $name, callable $instantiator ) {
280  if ( !$this->hasService( $name ) ) {
281  throw new NoSuchServiceException( $name );
282  }
283 
284  if ( isset( $this->services[$name] ) ) {
285  throw new CannotReplaceActiveServiceException( $name );
286  }
287 
288  $this->serviceInstantiators[$name] = $instantiator;
289  unset( $this->disabled[$name] );
290  }
291 
318  public function addServiceManipulator( string $name, callable $manipulator ) {
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 
349  public function disableService( $name ) {
350  $this->resetService( $name );
351 
352  $this->disabled[$name] = true;
353  }
354 
378  final protected function resetService( string $name, $destroy = true ) {
379  $instance = $this->peekService( $name );
380 
381  if ( $destroy && $instance instanceof DestructibleService ) {
382  $instance->destroy();
383  }
384 
385  unset( $this->services[$name] );
386  unset( $this->disabled[$name] );
387  }
388 
409  public function getService( $name ) {
410  if ( $this->destroyed ) {
411  throw new ContainerDisabledException();
412  }
413 
414  if ( isset( $this->disabled[$name] ) ) {
415  throw new ServiceDisabledException( $name );
416  }
417 
418  if ( !isset( $this->services[$name] ) ) {
419  $this->services[$name] = $this->createService( $name );
420  }
421 
422  return $this->services[$name];
423  }
424 
426  public function get( $name ) {
427  return $this->getService( $name );
428  }
429 
437  private function createService( $name ) {
438  if ( isset( $this->serviceInstantiators[$name] ) ) {
439  if ( isset( $this->servicesBeingCreated[$name] ) ) {
441  "Circular dependency when creating service! " .
442  implode( ' -> ', array_keys( $this->servicesBeingCreated ) ) . " -> $name" );
443  }
444  $this->servicesBeingCreated[$name] = true;
445  $removeFromStack = new ScopedCallback( function () use ( $name ) {
446  unset( $this->servicesBeingCreated[$name] );
447  } );
448 
449  $service = ( $this->serviceInstantiators[$name] )(
450  $this,
451  ...$this->extraInstantiationParams
452  );
453 
454  if ( isset( $this->serviceManipulators[$name] ) ) {
455  foreach ( $this->serviceManipulators[$name] as $callback ) {
456  $ret = call_user_func_array(
457  $callback,
458  array_merge( [ $service, $this ], $this->extraInstantiationParams )
459  );
460 
461  // If the manipulator callback returns an object, that object replaces
462  // the original service instance. This allows the manipulator to wrap
463  // or fully replace the service.
464  if ( $ret !== null ) {
465  $service = $ret;
466  }
467  }
468  }
469 
470  $removeFromStack->consume();
471 
472  // NOTE: when adding more wiring logic here, make sure importWiring() is kept in sync!
473  } else {
474  throw new NoSuchServiceException( $name );
475  }
476 
477  return $service;
478  }
479 
485  public function isServiceDisabled( $name ) {
486  return isset( $this->disabled[$name] );
487  }
488 }
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:36
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:485
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:38
$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\loadWiringFiles
loadWiringFiles(array $wiringFiles)
Definition: ServiceContainer.php:125
Wikimedia\Services\NoSuchServiceException
Exception thrown when the requested service is not known.
Definition: NoSuchServiceException.php:36
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\createService
createService( $name)
Definition: ServiceContainer.php:437
Wikimedia\Services\ServiceContainer\addServiceManipulator
addServiceManipulator(string $name, callable $manipulator)
Add a service manipulator callback for the given service.
Definition: ServiceContainer.php:318
Wikimedia\Services\ServiceDisabledException
Exception thrown when trying to access a disabled service.
Definition: ServiceDisabledException.php:36
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:349
Wikimedia\Services\ServiceContainer\defineService
defineService(string $name, callable $instantiator)
Define a new service.
Definition: ServiceContainer.php:252
Wikimedia\Services\ContainerDisabledException
Exception thrown when trying to access a service on a disabled container or factory.
Definition: ContainerDisabledException.php:36
Wikimedia\Services\ServiceContainer\$destroyed
bool $destroyed
Definition: ServiceContainer.php:79
Wikimedia\Services
Definition: CannotReplaceActiveServiceException.php:3
Wikimedia\Services\ServiceContainer\resetService
resetService(string $name, $destroy=true)
Resets a service by dropping the service instance.
Definition: ServiceContainer.php:378
Wikimedia\Services\RecursiveServiceDependencyException
Exception thrown when trying to instantiate a currently instantiating service.
Definition: RecursiveServiceDependencyException.php:35
Wikimedia\Services\ServiceContainer\getService
getService( $name)
Returns a service object of the kind associated with $name.
Definition: ServiceContainer.php:409
Wikimedia\Services\ServiceContainer\redefineService
redefineService(string $name, callable $instantiator)
Replace an already defined service.
Definition: ServiceContainer.php:279
Wikimedia\Services\ServiceContainer\$extraInstantiationParams
array $extraInstantiationParams
Definition: ServiceContainer.php:74