ServiceContainer.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <?php
  2. namespace MediaWiki\Services;
  3. use InvalidArgumentException;
  4. use RuntimeException;
  5. use Wikimedia\Assert\Assert;
  6. /**
  7. * Generic service container.
  8. *
  9. * This program is free software; you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation; either version 2 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License along
  20. * with this program; if not, write to the Free Software Foundation, Inc.,
  21. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. * http://www.gnu.org/copyleft/gpl.html
  23. *
  24. * @file
  25. *
  26. * @since 1.27
  27. */
  28. /**
  29. * ServiceContainer provides a generic service to manage named services using
  30. * lazy instantiation based on instantiator callback functions.
  31. *
  32. * Services managed by an instance of ServiceContainer may or may not implement
  33. * a common interface.
  34. *
  35. * @note When using ServiceContainer to manage a set of services, consider
  36. * creating a wrapper or a subclass that provides access to the services via
  37. * getter methods with more meaningful names and more specific return type
  38. * declarations.
  39. *
  40. * @see docs/injection.txt for an overview of using dependency injection in the
  41. * MediaWiki code base.
  42. */
  43. class ServiceContainer implements DestructibleService {
  44. /**
  45. * @var object[]
  46. */
  47. private $services = [];
  48. /**
  49. * @var callable[]
  50. */
  51. private $serviceInstantiators = [];
  52. /**
  53. * @var bool[] disabled status, per service name
  54. */
  55. private $disabled = [];
  56. /**
  57. * @var array
  58. */
  59. private $extraInstantiationParams;
  60. /**
  61. * @var bool
  62. */
  63. private $destroyed = false;
  64. /**
  65. * @param array $extraInstantiationParams Any additional parameters to be passed to the
  66. * instantiator function when creating a service. This is typically used to provide
  67. * access to additional ServiceContainers or Config objects.
  68. */
  69. public function __construct( array $extraInstantiationParams = [] ) {
  70. $this->extraInstantiationParams = $extraInstantiationParams;
  71. }
  72. /**
  73. * Destroys all contained service instances that implement the DestructibleService
  74. * interface. This will render all services obtained from this MediaWikiServices
  75. * instance unusable. In particular, this will disable access to the storage backend
  76. * via any of these services. Any future call to getService() will throw an exception.
  77. *
  78. * @see resetGlobalInstance()
  79. */
  80. public function destroy() {
  81. foreach ( $this->getServiceNames() as $name ) {
  82. $service = $this->peekService( $name );
  83. if ( $service !== null && $service instanceof DestructibleService ) {
  84. $service->destroy();
  85. }
  86. }
  87. $this->destroyed = true;
  88. }
  89. /**
  90. * @param array $wiringFiles A list of PHP files to load wiring information from.
  91. * Each file is loaded using PHP's include mechanism. Each file is expected to
  92. * return an associative array that maps service names to instantiator functions.
  93. */
  94. public function loadWiringFiles( array $wiringFiles ) {
  95. foreach ( $wiringFiles as $file ) {
  96. // the wiring file is required to return an array of instantiators.
  97. $wiring = require $file;
  98. Assert::postcondition(
  99. is_array( $wiring ),
  100. "Wiring file $file is expected to return an array!"
  101. );
  102. $this->applyWiring( $wiring );
  103. }
  104. }
  105. /**
  106. * Registers multiple services (aka a "wiring").
  107. *
  108. * @param array $serviceInstantiators An associative array mapping service names to
  109. * instantiator functions.
  110. */
  111. public function applyWiring( array $serviceInstantiators ) {
  112. Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' );
  113. foreach ( $serviceInstantiators as $name => $instantiator ) {
  114. $this->defineService( $name, $instantiator );
  115. }
  116. }
  117. /**
  118. * Imports all wiring defined in $container. Wiring defined in $container
  119. * will override any wiring already defined locally. However, already
  120. * existing service instances will be preserved.
  121. *
  122. * @since 1.28
  123. *
  124. * @param ServiceContainer $container
  125. * @param string[] $skip A list of service names to skip during import
  126. */
  127. public function importWiring( ServiceContainer $container, $skip = [] ) {
  128. $newInstantiators = array_diff_key(
  129. $container->serviceInstantiators,
  130. array_flip( $skip )
  131. );
  132. $this->serviceInstantiators = array_merge(
  133. $this->serviceInstantiators,
  134. $newInstantiators
  135. );
  136. }
  137. /**
  138. * Returns true if a service is defined for $name, that is, if a call to getService( $name )
  139. * would return a service instance.
  140. *
  141. * @param string $name
  142. *
  143. * @return bool
  144. */
  145. public function hasService( $name ) {
  146. return isset( $this->serviceInstantiators[$name] );
  147. }
  148. /**
  149. * Returns the service instance for $name only if that service has already been instantiated.
  150. * This is intended for situations where services get destroyed/cleaned up, so we can
  151. * avoid creating a service just to destroy it again.
  152. *
  153. * @note This is intended for internal use and for test fixtures.
  154. * Application logic should use getService() instead.
  155. *
  156. * @see getService().
  157. *
  158. * @param string $name
  159. *
  160. * @return object|null The service instance, or null if the service has not yet been instantiated.
  161. * @throws RuntimeException if $name does not refer to a known service.
  162. */
  163. public function peekService( $name ) {
  164. if ( !$this->hasService( $name ) ) {
  165. throw new NoSuchServiceException( $name );
  166. }
  167. return isset( $this->services[$name] ) ? $this->services[$name] : null;
  168. }
  169. /**
  170. * @return string[]
  171. */
  172. public function getServiceNames() {
  173. return array_keys( $this->serviceInstantiators );
  174. }
  175. /**
  176. * Define a new service. The service must not be known already.
  177. *
  178. * @see getService().
  179. * @see replaceService().
  180. *
  181. * @param string $name The name of the service to register, for use with getService().
  182. * @param callable $instantiator Callback that returns a service instance.
  183. * Will be called with this MediaWikiServices instance as the only parameter.
  184. * Any extra instantiation parameters provided to the constructor will be
  185. * passed as subsequent parameters when invoking the instantiator.
  186. *
  187. * @throws RuntimeException if there is already a service registered as $name.
  188. */
  189. public function defineService( $name, callable $instantiator ) {
  190. Assert::parameterType( 'string', $name, '$name' );
  191. if ( $this->hasService( $name ) ) {
  192. throw new ServiceAlreadyDefinedException( $name );
  193. }
  194. $this->serviceInstantiators[$name] = $instantiator;
  195. }
  196. /**
  197. * Replace an already defined service.
  198. *
  199. * @see defineService().
  200. *
  201. * @note This causes any previously instantiated instance of the service to be discarded.
  202. *
  203. * @param string $name The name of the service to register.
  204. * @param callable $instantiator Callback function that returns a service instance.
  205. * Will be called with this MediaWikiServices instance as the only parameter.
  206. * The instantiator must return a service compatible with the originally defined service.
  207. * Any extra instantiation parameters provided to the constructor will be
  208. * passed as subsequent parameters when invoking the instantiator.
  209. *
  210. * @throws RuntimeException if $name is not a known service.
  211. */
  212. public function redefineService( $name, callable $instantiator ) {
  213. Assert::parameterType( 'string', $name, '$name' );
  214. if ( !$this->hasService( $name ) ) {
  215. throw new NoSuchServiceException( $name );
  216. }
  217. if ( isset( $this->services[$name] ) ) {
  218. throw new CannotReplaceActiveServiceException( $name );
  219. }
  220. $this->serviceInstantiators[$name] = $instantiator;
  221. unset( $this->disabled[$name] );
  222. }
  223. /**
  224. * Disables a service.
  225. *
  226. * @note Attempts to call getService() for a disabled service will result
  227. * in a DisabledServiceException. Calling peekService for a disabled service will
  228. * return null. Disabled services are listed by getServiceNames(). A disabled service
  229. * can be enabled again using redefineService().
  230. *
  231. * @note If the service was already active (that is, instantiated) when getting disabled,
  232. * and the service instance implements DestructibleService, destroy() is called on the
  233. * service instance.
  234. *
  235. * @see redefineService()
  236. * @see resetService()
  237. *
  238. * @param string $name The name of the service to disable.
  239. *
  240. * @throws RuntimeException if $name is not a known service.
  241. */
  242. public function disableService( $name ) {
  243. $this->resetService( $name );
  244. $this->disabled[$name] = true;
  245. }
  246. /**
  247. * Resets a service by dropping the service instance.
  248. * If the service instances implements DestructibleService, destroy()
  249. * is called on the service instance.
  250. *
  251. * @warning This is generally unsafe! Other services may still retain references
  252. * to the stale service instance, leading to failures and inconsistencies. Subclasses
  253. * may use this method to reset specific services under specific instances, but
  254. * it should not be exposed to application logic.
  255. *
  256. * @note This is declared final so subclasses can not interfere with the expectations
  257. * disableService() has when calling resetService().
  258. *
  259. * @see redefineService()
  260. * @see disableService().
  261. *
  262. * @param string $name The name of the service to reset.
  263. * @param bool $destroy Whether the service instance should be destroyed if it exists.
  264. * When set to false, any existing service instance will effectively be detached
  265. * from the container.
  266. *
  267. * @throws RuntimeException if $name is not a known service.
  268. */
  269. final protected function resetService( $name, $destroy = true ) {
  270. Assert::parameterType( 'string', $name, '$name' );
  271. $instance = $this->peekService( $name );
  272. if ( $destroy && $instance instanceof DestructibleService ) {
  273. $instance->destroy();
  274. }
  275. unset( $this->services[$name] );
  276. unset( $this->disabled[$name] );
  277. }
  278. /**
  279. * Returns a service object of the kind associated with $name.
  280. * Services instances are instantiated lazily, on demand.
  281. * This method may or may not return the same service instance
  282. * when called multiple times with the same $name.
  283. *
  284. * @note Rather than calling this method directly, it is recommended to provide
  285. * getters with more meaningful names and more specific return types, using
  286. * a subclass or wrapper.
  287. *
  288. * @see redefineService().
  289. *
  290. * @param string $name The service name
  291. *
  292. * @throws NoSuchServiceException if $name is not a known service.
  293. * @throws ContainerDisabledException if this container has already been destroyed.
  294. * @throws ServiceDisabledException if the requested service has been disabled.
  295. *
  296. * @return object The service instance
  297. */
  298. public function getService( $name ) {
  299. if ( $this->destroyed ) {
  300. throw new ContainerDisabledException();
  301. }
  302. if ( isset( $this->disabled[$name] ) ) {
  303. throw new ServiceDisabledException( $name );
  304. }
  305. if ( !isset( $this->services[$name] ) ) {
  306. $this->services[$name] = $this->createService( $name );
  307. }
  308. return $this->services[$name];
  309. }
  310. /**
  311. * @param string $name
  312. *
  313. * @throws InvalidArgumentException if $name is not a known service.
  314. * @return object
  315. */
  316. private function createService( $name ) {
  317. if ( isset( $this->serviceInstantiators[$name] ) ) {
  318. $service = call_user_func_array(
  319. $this->serviceInstantiators[$name],
  320. array_merge( [ $this ], $this->extraInstantiationParams )
  321. );
  322. // NOTE: when adding more wiring logic here, make sure copyWiring() is kept in sync!
  323. } else {
  324. throw new NoSuchServiceException( $name );
  325. }
  326. return $service;
  327. }
  328. /**
  329. * @param string $name
  330. * @return bool Whether the service is disabled
  331. * @since 1.28
  332. */
  333. public function isServiceDisabled( $name ) {
  334. return isset( $this->disabled[$name] );
  335. }
  336. }