From: Lucas Werkmeister Date: Mon, 10 Dec 2018 17:33:38 +0000 (+0100) Subject: Fully extract services framework as a library X-Git-Tag: 1.34.0-rc.0~3071 X-Git-Url: http://git.cyclocoop.org/%7B%24admin_url%7Dmes_infos.php?a=commitdiff_plain;h=7585985a463964d56ebb4b56b31c467fd98a7684;p=lhc%2Fweb%2Fwiklou.git Fully extract services framework as a library This code doesn’t use any MediaWiki-specific code, so rename MediaWiki\Services to Wikimedia\Services and move it below libs/. (Of course, this does not apply to the MediaWikiServices subclass.) Class aliases are added to retain backwards compatibity for now. Bug: T211608 Change-Id: Ic14ea28ef21c359695b309d4293dbaaf5deedc09 --- diff --git a/RELEASE-NOTES-1.33 b/RELEASE-NOTES-1.33 index c0dd84f56e..f3899e0085 100644 --- a/RELEASE-NOTES-1.33 +++ b/RELEASE-NOTES-1.33 @@ -201,6 +201,8 @@ because of Phabricator reports. Title::getUserPermissionsErrors() and Title::userCan(). Previously, the method was only called in Action::checkCanExecute(). Actions should ensure that their requiresUnblock() returns the proper result (the default is `true`). +* (T211608) The MediaWiki\Services namespace has been renamed to + Wikimedia\Services. The old name is still supported, but deprecated. * … === Other changes in 1.33 === diff --git a/autoload.php b/autoload.php index 91be2e71b9..afc187fd85 100644 --- a/autoload.php +++ b/autoload.php @@ -903,6 +903,14 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\OutputHandler' => __DIR__ . '/includes/OutputHandler.php', 'MediaWiki\\ProcOpenError' => __DIR__ . '/includes/exception/ProcOpenError.php', 'MediaWiki\\Search\\ParserOutputSearchDataExtractor' => __DIR__ . '/includes/search/ParserOutputSearchDataExtractor.php', + 'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/libs/services/CannotReplaceActiveServiceException.php', + 'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . '/includes/libs/services/ContainerDisabledException.php', + 'MediaWiki\\Services\\DestructibleService' => __DIR__ . '/includes/libs/services/DestructibleService.php', + 'MediaWiki\\Services\\NoSuchServiceException' => __DIR__ . '/includes/libs/services/NoSuchServiceException.php', + 'MediaWiki\\Services\\SalvageableService' => __DIR__ . '/includes/libs/services/SalvageableService.php', + 'MediaWiki\\Services\\ServiceAlreadyDefinedException' => __DIR__ . '/includes/libs/services/ServiceAlreadyDefinedException.php', + 'MediaWiki\\Services\\ServiceContainer' => __DIR__ . '/includes/libs/services/ServiceContainer.php', + 'MediaWiki\\Services\\ServiceDisabledException' => __DIR__ . '/includes/libs/services/ServiceDisabledException.php', 'MediaWiki\\ShellDisabledError' => __DIR__ . '/includes/exception/ShellDisabledError.php', 'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php', 'MediaWiki\\Special\\SpecialPageFactory' => __DIR__ . '/includes/specialpage/SpecialPageFactory.php', diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 677fd01b54..9dbc9eb006 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -136,12 +136,12 @@ class AutoLoader { 'MediaWiki\\Linker\\' => __DIR__ . '/linker/', 'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/', 'MediaWiki\\Revision\\' => __DIR__ . '/Revision/', - 'MediaWiki\\Services\\' => __DIR__ . '/services/', 'MediaWiki\\Session\\' => __DIR__ . '/session/', 'MediaWiki\\Shell\\' => __DIR__ . '/shell/', 'MediaWiki\\Sparql\\' => __DIR__ . '/sparql/', 'MediaWiki\\Storage\\' => __DIR__ . '/Storage/', 'MediaWiki\\Tidy\\' => __DIR__ . '/tidy/', + 'Wikimedia\\Services\\' => __DIR__ . '/libs/services/', ]; } } diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 0e36b22367..4abd729d45 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -36,9 +36,6 @@ use MediaHandlerFactory; use MediaWiki\Config\ConfigRepository; use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkRendererFactory; -use MediaWiki\Services\SalvageableService; -use MediaWiki\Services\ServiceContainer; -use MediaWiki\Services\NoSuchServiceException; use MWException; use MimeAnalyzer; use ObjectCache; @@ -58,6 +55,9 @@ use SkinFactory; use TitleFormatter; use TitleParser; use VirtualRESTServiceClient; +use Wikimedia\Services\SalvageableService; +use Wikimedia\Services\ServiceContainer; +use Wikimedia\Services\NoSuchServiceException; use MediaWiki\Interwiki\InterwikiLookup; use MagicWordFactory; diff --git a/includes/config/ConfigFactory.php b/includes/config/ConfigFactory.php index 2c7afdae07..769364fd1f 100644 --- a/includes/config/ConfigFactory.php +++ b/includes/config/ConfigFactory.php @@ -20,8 +20,8 @@ * * @file */ -use MediaWiki\Services\SalvageableService; use Wikimedia\Assert\Assert; +use Wikimedia\Services\SalvageableService; /** * Factory class to create Config objects diff --git a/includes/config/ConfigRepository.php b/includes/config/ConfigRepository.php index c87a3440b6..96dc51c118 100644 --- a/includes/config/ConfigRepository.php +++ b/includes/config/ConfigRepository.php @@ -22,8 +22,8 @@ namespace MediaWiki\Config; -use MediaWiki\Services\SalvageableService; use Wikimedia\Assert\Assert; +use Wikimedia\Services\SalvageableService; /** * Object which holds currently registered configuration options. diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index 029f67d57a..5a3d77adf4 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -694,7 +694,7 @@ abstract class Installer { 'enableSectionEditLinks' => false, 'unwrap' => true, ] ); - } catch ( MediaWiki\Services\ServiceDisabledException $e ) { + } catch ( Wikimedia\Services\ServiceDisabledException $e ) { $html = ' ' . htmlspecialchars( $text ); } diff --git a/includes/libs/services/CannotReplaceActiveServiceException.php b/includes/libs/services/CannotReplaceActiveServiceException.php new file mode 100644 index 0000000000..ee0d7d00ae --- /dev/null +++ b/includes/libs/services/CannotReplaceActiveServiceException.php @@ -0,0 +1,50 @@ +destroy() + * after carefully detaching all relevant resources. + * + * @param SalvageableService $other The object to salvage state from. $other must have the + * exact same type as $this. + */ + public function salvage( SalvageableService $other ); + +} + +/** + * Retain the old class name for backwards compatibility. + * @deprecated since 1.33 + */ +class_alias( SalvageableService::class, 'MediaWiki\Services\SalvageableService' ); diff --git a/includes/libs/services/ServiceAlreadyDefinedException.php b/includes/libs/services/ServiceAlreadyDefinedException.php new file mode 100644 index 0000000000..39c8384f3e --- /dev/null +++ b/includes/libs/services/ServiceAlreadyDefinedException.php @@ -0,0 +1,52 @@ +extraInstantiationParams = $extraInstantiationParams; + } + + /** + * Destroys all contained service instances that implement the DestructibleService + * interface. This will render all services obtained from this ServiceContainer + * instance unusable. In particular, this will disable access to the storage backend + * via any of these services. Any future call to getService() will throw an exception. + * + * @see resetGlobalInstance() + */ + public function destroy() { + foreach ( $this->getServiceNames() as $name ) { + $service = $this->peekService( $name ); + if ( $service !== null && $service instanceof DestructibleService ) { + $service->destroy(); + } + } + + // Break circular references due to the $this reference in closures, by + // erasing the instantiator array. This allows the ServiceContainer to + // be deleted when it goes out of scope. + $this->serviceInstantiators = []; + // Also remove the services themselves, to avoid confusion. + $this->services = []; + $this->destroyed = true; + } + + /** + * @param array $wiringFiles A list of PHP files to load wiring information from. + * Each file is loaded using PHP's include mechanism. Each file is expected to + * return an associative array that maps service names to instantiator functions. + */ + public function loadWiringFiles( array $wiringFiles ) { + foreach ( $wiringFiles as $file ) { + // the wiring file is required to return an array of instantiators. + $wiring = require $file; + + Assert::postcondition( + is_array( $wiring ), + "Wiring file $file is expected to return an array!" + ); + + $this->applyWiring( $wiring ); + } + } + + /** + * Registers multiple services (aka a "wiring"). + * + * @param array $serviceInstantiators An associative array mapping service names to + * instantiator functions. + */ + public function applyWiring( array $serviceInstantiators ) { + Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' ); + + foreach ( $serviceInstantiators as $name => $instantiator ) { + $this->defineService( $name, $instantiator ); + } + } + + /** + * Imports all wiring defined in $container. Wiring defined in $container + * will override any wiring already defined locally. However, already + * existing service instances will be preserved. + * + * @since 1.28 + * + * @param ServiceContainer $container + * @param string[] $skip A list of service names to skip during import + */ + public function importWiring( ServiceContainer $container, $skip = [] ) { + $newInstantiators = array_diff_key( + $container->serviceInstantiators, + array_flip( $skip ) + ); + + $this->serviceInstantiators = array_merge( + $this->serviceInstantiators, + $newInstantiators + ); + + $newManipulators = array_diff( + array_keys( $container->serviceManipulators ), + $skip + ); + + foreach ( $newManipulators as $name ) { + if ( isset( $this->serviceManipulators[$name] ) ) { + $this->serviceManipulators[$name] = array_merge( + $this->serviceManipulators[$name], + $container->serviceManipulators[$name] + ); + } else { + $this->serviceManipulators[$name] = $container->serviceManipulators[$name]; + } + } + } + + /** + * Returns true if a service is defined for $name, that is, if a call to getService( $name ) + * would return a service instance. + * + * @param string $name + * + * @return bool + */ + public function hasService( $name ) { + return isset( $this->serviceInstantiators[$name] ); + } + + /** + * Returns the service instance for $name only if that service has already been instantiated. + * This is intended for situations where services get destroyed/cleaned up, so we can + * avoid creating a service just to destroy it again. + * + * @note This is intended for internal use and for test fixtures. + * Application logic should use getService() instead. + * + * @see getService(). + * + * @param string $name + * + * @return object|null The service instance, or null if the service has not yet been instantiated. + * @throws RuntimeException if $name does not refer to a known service. + */ + public function peekService( $name ) { + if ( !$this->hasService( $name ) ) { + throw new NoSuchServiceException( $name ); + } + + return $this->services[$name] ?? null; + } + + /** + * @return string[] + */ + public function getServiceNames() { + return array_keys( $this->serviceInstantiators ); + } + + /** + * Define a new service. The service must not be known already. + * + * @see getService(). + * @see redefineService(). + * + * @param string $name The name of the service to register, for use with getService(). + * @param callable $instantiator Callback that returns a service instance. + * Will be called with this ServiceContainer instance as the only parameter. + * Any extra instantiation parameters provided to the constructor will be + * passed as subsequent parameters when invoking the instantiator. + * + * @throws RuntimeException if there is already a service registered as $name. + */ + public function defineService( $name, callable $instantiator ) { + Assert::parameterType( 'string', $name, '$name' ); + + if ( $this->hasService( $name ) ) { + throw new ServiceAlreadyDefinedException( $name ); + } + + $this->serviceInstantiators[$name] = $instantiator; + } + + /** + * Replace an already defined service. + * + * @see defineService(). + * + * @note This will fail if the service was already instantiated. If the service was previously + * disabled, it will be re-enabled by this call. Any manipulators registered for the service + * will remain in place. + * + * @param string $name The name of the service to register. + * @param callable $instantiator Callback function that returns a service instance. + * Will be called with this ServiceContainer instance as the only parameter. + * The instantiator must return a service compatible with the originally defined service. + * Any extra instantiation parameters provided to the constructor will be + * passed as subsequent parameters when invoking the instantiator. + * + * @throws NoSuchServiceException if $name is not a known service. + * @throws CannotReplaceActiveServiceException if the service was already instantiated. + */ + public function redefineService( $name, callable $instantiator ) { + Assert::parameterType( 'string', $name, '$name' ); + + if ( !$this->hasService( $name ) ) { + throw new NoSuchServiceException( $name ); + } + + if ( isset( $this->services[$name] ) ) { + throw new CannotReplaceActiveServiceException( $name ); + } + + $this->serviceInstantiators[$name] = $instantiator; + unset( $this->disabled[$name] ); + } + + /** + * Add a service manipulator callback for the given service. + * This method may be used by extensions that need to wrap, replace, or re-configure a + * service. It would typically be called from a MediaWikiServices hook handler. + * + * The manipulator callback is called just after the service is instantiated. + * It can call methods on the service to change configuration, or wrap or otherwise + * replace it. + * + * @see defineService(). + * @see redefineService(). + * + * @note This will fail if the service was already instantiated. + * + * @since 1.32 + * + * @param string $name The name of the service to manipulate. + * @param callable $manipulator Callback function that manipulates, wraps or replaces a + * service instance. The callback receives the new service instance and this + * ServiceContainer as parameters, as well as any extra instantiation parameters specified + * when constructing this ServiceContainer. If the callback returns a value, that + * value replaces the original service instance. + * + * @throws NoSuchServiceException if $name is not a known service. + * @throws CannotReplaceActiveServiceException if the service was already instantiated. + */ + public function addServiceManipulator( $name, callable $manipulator ) { + Assert::parameterType( 'string', $name, '$name' ); + + if ( !$this->hasService( $name ) ) { + throw new NoSuchServiceException( $name ); + } + + if ( isset( $this->services[$name] ) ) { + throw new CannotReplaceActiveServiceException( $name ); + } + + $this->serviceManipulators[$name][] = $manipulator; + } + + /** + * Disables a service. + * + * @note Attempts to call getService() for a disabled service will result + * in a DisabledServiceException. Calling peekService for a disabled service will + * return null. Disabled services are listed by getServiceNames(). A disabled service + * can be enabled again using redefineService(). + * + * @note If the service was already active (that is, instantiated) when getting disabled, + * and the service instance implements DestructibleService, destroy() is called on the + * service instance. + * + * @see redefineService() + * @see resetService() + * + * @param string $name The name of the service to disable. + * + * @throws RuntimeException if $name is not a known service. + */ + public function disableService( $name ) { + $this->resetService( $name ); + + $this->disabled[$name] = true; + } + + /** + * Resets a service by dropping the service instance. + * If the service instances implements DestructibleService, destroy() + * is called on the service instance. + * + * @warning This is generally unsafe! Other services may still retain references + * to the stale service instance, leading to failures and inconsistencies. Subclasses + * may use this method to reset specific services under specific instances, but + * it should not be exposed to application logic. + * + * @note This is declared final so subclasses can not interfere with the expectations + * disableService() has when calling resetService(). + * + * @see redefineService() + * @see disableService(). + * + * @param string $name The name of the service to reset. + * @param bool $destroy Whether the service instance should be destroyed if it exists. + * When set to false, any existing service instance will effectively be detached + * from the container. + * + * @throws RuntimeException if $name is not a known service. + */ + final protected function resetService( $name, $destroy = true ) { + Assert::parameterType( 'string', $name, '$name' ); + + $instance = $this->peekService( $name ); + + if ( $destroy && $instance instanceof DestructibleService ) { + $instance->destroy(); + } + + unset( $this->services[$name] ); + unset( $this->disabled[$name] ); + } + + /** + * Returns a service object of the kind associated with $name. + * Services instances are instantiated lazily, on demand. + * This method may or may not return the same service instance + * when called multiple times with the same $name. + * + * @note Rather than calling this method directly, it is recommended to provide + * getters with more meaningful names and more specific return types, using + * a subclass or wrapper. + * + * @see redefineService(). + * + * @param string $name The service name + * + * @throws NoSuchServiceException if $name is not a known service. + * @throws ContainerDisabledException if this container has already been destroyed. + * @throws ServiceDisabledException if the requested service has been disabled. + * + * @return object The service instance + */ + public function getService( $name ) { + if ( $this->destroyed ) { + throw new ContainerDisabledException(); + } + + if ( isset( $this->disabled[$name] ) ) { + throw new ServiceDisabledException( $name ); + } + + if ( !isset( $this->services[$name] ) ) { + $this->services[$name] = $this->createService( $name ); + } + + return $this->services[$name]; + } + + /** + * @param string $name + * + * @throws InvalidArgumentException if $name is not a known service. + * @return object + */ + private function createService( $name ) { + if ( isset( $this->serviceInstantiators[$name] ) ) { + $service = ( $this->serviceInstantiators[$name] )( + $this, + ...$this->extraInstantiationParams + ); + + if ( isset( $this->serviceManipulators[$name] ) ) { + foreach ( $this->serviceManipulators[$name] as $callback ) { + $ret = call_user_func_array( + $callback, + array_merge( [ $service, $this ], $this->extraInstantiationParams ) + ); + + // If the manipulator callback returns an object, that object replaces + // the original service instance. This allows the manipulator to wrap + // or fully replace the service. + if ( $ret !== null ) { + $service = $ret; + } + } + } + + // NOTE: when adding more wiring logic here, make sure importWiring() is kept in sync! + } else { + throw new NoSuchServiceException( $name ); + } + + return $service; + } + + /** + * @param string $name + * @return bool Whether the service is disabled + * @since 1.28 + */ + public function isServiceDisabled( $name ) { + return isset( $this->disabled[$name] ); + } +} + +/** + * Retain the old class name for backwards compatibility. + * @deprecated since 1.33 + */ +class_alias( ServiceContainer::class, 'MediaWiki\Services\ServiceContainer' ); diff --git a/includes/libs/services/ServiceDisabledException.php b/includes/libs/services/ServiceDisabledException.php new file mode 100644 index 0000000000..86b927bddf --- /dev/null +++ b/includes/libs/services/ServiceDisabledException.php @@ -0,0 +1,49 @@ +destroy() - * after carefully detaching all relevant resources. - * - * @param SalvageableService $other The object to salvage state from. $other must have the - * exact same type as $this. - */ - public function salvage( SalvageableService $other ); - -} diff --git a/includes/services/ServiceAlreadyDefinedException.php b/includes/services/ServiceAlreadyDefinedException.php deleted file mode 100644 index c6344d3955..0000000000 --- a/includes/services/ServiceAlreadyDefinedException.php +++ /dev/null @@ -1,45 +0,0 @@ -extraInstantiationParams = $extraInstantiationParams; - } - - /** - * Destroys all contained service instances that implement the DestructibleService - * interface. This will render all services obtained from this MediaWikiServices - * instance unusable. In particular, this will disable access to the storage backend - * via any of these services. Any future call to getService() will throw an exception. - * - * @see resetGlobalInstance() - */ - public function destroy() { - foreach ( $this->getServiceNames() as $name ) { - $service = $this->peekService( $name ); - if ( $service !== null && $service instanceof DestructibleService ) { - $service->destroy(); - } - } - - // Break circular references due to the $this reference in closures, by - // erasing the instantiator array. This allows the ServiceContainer to - // be deleted when it goes out of scope. - $this->serviceInstantiators = []; - // Also remove the services themselves, to avoid confusion. - $this->services = []; - $this->destroyed = true; - } - - /** - * @param array $wiringFiles A list of PHP files to load wiring information from. - * Each file is loaded using PHP's include mechanism. Each file is expected to - * return an associative array that maps service names to instantiator functions. - */ - public function loadWiringFiles( array $wiringFiles ) { - foreach ( $wiringFiles as $file ) { - // the wiring file is required to return an array of instantiators. - $wiring = require $file; - - Assert::postcondition( - is_array( $wiring ), - "Wiring file $file is expected to return an array!" - ); - - $this->applyWiring( $wiring ); - } - } - - /** - * Registers multiple services (aka a "wiring"). - * - * @param array $serviceInstantiators An associative array mapping service names to - * instantiator functions. - */ - public function applyWiring( array $serviceInstantiators ) { - Assert::parameterElementType( 'callable', $serviceInstantiators, '$serviceInstantiators' ); - - foreach ( $serviceInstantiators as $name => $instantiator ) { - $this->defineService( $name, $instantiator ); - } - } - - /** - * Imports all wiring defined in $container. Wiring defined in $container - * will override any wiring already defined locally. However, already - * existing service instances will be preserved. - * - * @since 1.28 - * - * @param ServiceContainer $container - * @param string[] $skip A list of service names to skip during import - */ - public function importWiring( ServiceContainer $container, $skip = [] ) { - $newInstantiators = array_diff_key( - $container->serviceInstantiators, - array_flip( $skip ) - ); - - $this->serviceInstantiators = array_merge( - $this->serviceInstantiators, - $newInstantiators - ); - - $newManipulators = array_diff( - array_keys( $container->serviceManipulators ), - $skip - ); - - foreach ( $newManipulators as $name ) { - if ( isset( $this->serviceManipulators[$name] ) ) { - $this->serviceManipulators[$name] = array_merge( - $this->serviceManipulators[$name], - $container->serviceManipulators[$name] - ); - } else { - $this->serviceManipulators[$name] = $container->serviceManipulators[$name]; - } - } - } - - /** - * Returns true if a service is defined for $name, that is, if a call to getService( $name ) - * would return a service instance. - * - * @param string $name - * - * @return bool - */ - public function hasService( $name ) { - return isset( $this->serviceInstantiators[$name] ); - } - - /** - * Returns the service instance for $name only if that service has already been instantiated. - * This is intended for situations where services get destroyed/cleaned up, so we can - * avoid creating a service just to destroy it again. - * - * @note This is intended for internal use and for test fixtures. - * Application logic should use getService() instead. - * - * @see getService(). - * - * @param string $name - * - * @return object|null The service instance, or null if the service has not yet been instantiated. - * @throws RuntimeException if $name does not refer to a known service. - */ - public function peekService( $name ) { - if ( !$this->hasService( $name ) ) { - throw new NoSuchServiceException( $name ); - } - - return $this->services[$name] ?? null; - } - - /** - * @return string[] - */ - public function getServiceNames() { - return array_keys( $this->serviceInstantiators ); - } - - /** - * Define a new service. The service must not be known already. - * - * @see getService(). - * @see redefineService(). - * - * @param string $name The name of the service to register, for use with getService(). - * @param callable $instantiator Callback that returns a service instance. - * Will be called with this MediaWikiServices instance as the only parameter. - * Any extra instantiation parameters provided to the constructor will be - * passed as subsequent parameters when invoking the instantiator. - * - * @throws RuntimeException if there is already a service registered as $name. - */ - public function defineService( $name, callable $instantiator ) { - Assert::parameterType( 'string', $name, '$name' ); - - if ( $this->hasService( $name ) ) { - throw new ServiceAlreadyDefinedException( $name ); - } - - $this->serviceInstantiators[$name] = $instantiator; - } - - /** - * Replace an already defined service. - * - * @see defineService(). - * - * @note This will fail if the service was already instantiated. If the service was previously - * disabled, it will be re-enabled by this call. Any manipulators registered for the service - * will remain in place. - * - * @param string $name The name of the service to register. - * @param callable $instantiator Callback function that returns a service instance. - * Will be called with this MediaWikiServices instance as the only parameter. - * The instantiator must return a service compatible with the originally defined service. - * Any extra instantiation parameters provided to the constructor will be - * passed as subsequent parameters when invoking the instantiator. - * - * @throws NoSuchServiceException if $name is not a known service. - * @throws CannotReplaceActiveServiceException if the service was already instantiated. - */ - public function redefineService( $name, callable $instantiator ) { - Assert::parameterType( 'string', $name, '$name' ); - - if ( !$this->hasService( $name ) ) { - throw new NoSuchServiceException( $name ); - } - - if ( isset( $this->services[$name] ) ) { - throw new CannotReplaceActiveServiceException( $name ); - } - - $this->serviceInstantiators[$name] = $instantiator; - unset( $this->disabled[$name] ); - } - - /** - * Add a service manipulator callback for the given service. - * This method may be used by extensions that need to wrap, replace, or re-configure a - * service. It would typically be called from a MediaWikiServices hook handler. - * - * The manipulator callback is called just after the service is instantiated. - * It can call methods on the service to change configuration, or wrap or otherwise - * replace it. - * - * @see defineService(). - * @see redefineService(). - * - * @note This will fail if the service was already instantiated. - * - * @since 1.32 - * - * @param string $name The name of the service to manipulate. - * @param callable $manipulator Callback function that manipulates, wraps or replaces a - * service instance. The callback receives the new service instance and this the - * ServiceContainer as parameters, as well as any extra instantiation parameters specified - * when constructing this ServiceContainer. If the callback returns a value, that - * value replaces the original service instance. - * - * @throws NoSuchServiceException if $name is not a known service. - * @throws CannotReplaceActiveServiceException if the service was already instantiated. - */ - public function addServiceManipulator( $name, callable $manipulator ) { - Assert::parameterType( 'string', $name, '$name' ); - - if ( !$this->hasService( $name ) ) { - throw new NoSuchServiceException( $name ); - } - - if ( isset( $this->services[$name] ) ) { - throw new CannotReplaceActiveServiceException( $name ); - } - - $this->serviceManipulators[$name][] = $manipulator; - } - - /** - * Disables a service. - * - * @note Attempts to call getService() for a disabled service will result - * in a DisabledServiceException. Calling peekService for a disabled service will - * return null. Disabled services are listed by getServiceNames(). A disabled service - * can be enabled again using redefineService(). - * - * @note If the service was already active (that is, instantiated) when getting disabled, - * and the service instance implements DestructibleService, destroy() is called on the - * service instance. - * - * @see redefineService() - * @see resetService() - * - * @param string $name The name of the service to disable. - * - * @throws RuntimeException if $name is not a known service. - */ - public function disableService( $name ) { - $this->resetService( $name ); - - $this->disabled[$name] = true; - } - - /** - * Resets a service by dropping the service instance. - * If the service instances implements DestructibleService, destroy() - * is called on the service instance. - * - * @warning This is generally unsafe! Other services may still retain references - * to the stale service instance, leading to failures and inconsistencies. Subclasses - * may use this method to reset specific services under specific instances, but - * it should not be exposed to application logic. - * - * @note This is declared final so subclasses can not interfere with the expectations - * disableService() has when calling resetService(). - * - * @see redefineService() - * @see disableService(). - * - * @param string $name The name of the service to reset. - * @param bool $destroy Whether the service instance should be destroyed if it exists. - * When set to false, any existing service instance will effectively be detached - * from the container. - * - * @throws RuntimeException if $name is not a known service. - */ - final protected function resetService( $name, $destroy = true ) { - Assert::parameterType( 'string', $name, '$name' ); - - $instance = $this->peekService( $name ); - - if ( $destroy && $instance instanceof DestructibleService ) { - $instance->destroy(); - } - - unset( $this->services[$name] ); - unset( $this->disabled[$name] ); - } - - /** - * Returns a service object of the kind associated with $name. - * Services instances are instantiated lazily, on demand. - * This method may or may not return the same service instance - * when called multiple times with the same $name. - * - * @note Rather than calling this method directly, it is recommended to provide - * getters with more meaningful names and more specific return types, using - * a subclass or wrapper. - * - * @see redefineService(). - * - * @param string $name The service name - * - * @throws NoSuchServiceException if $name is not a known service. - * @throws ContainerDisabledException if this container has already been destroyed. - * @throws ServiceDisabledException if the requested service has been disabled. - * - * @return object The service instance - */ - public function getService( $name ) { - if ( $this->destroyed ) { - throw new ContainerDisabledException(); - } - - if ( isset( $this->disabled[$name] ) ) { - throw new ServiceDisabledException( $name ); - } - - if ( !isset( $this->services[$name] ) ) { - $this->services[$name] = $this->createService( $name ); - } - - return $this->services[$name]; - } - - /** - * @param string $name - * - * @throws InvalidArgumentException if $name is not a known service. - * @return object - */ - private function createService( $name ) { - if ( isset( $this->serviceInstantiators[$name] ) ) { - $service = ( $this->serviceInstantiators[$name] )( - $this, - ...$this->extraInstantiationParams - ); - - if ( isset( $this->serviceManipulators[$name] ) ) { - foreach ( $this->serviceManipulators[$name] as $callback ) { - $ret = call_user_func_array( - $callback, - array_merge( [ $service, $this ], $this->extraInstantiationParams ) - ); - - // If the manipulator callback returns an object, that object replaces - // the original service instance. This allows the manipulator to wrap - // or fully replace the service. - if ( $ret !== null ) { - $service = $ret; - } - } - } - - // NOTE: when adding more wiring logic here, make sure importWiring() is kept in sync! - } else { - throw new NoSuchServiceException( $name ); - } - - return $service; - } - - /** - * @param string $name - * @return bool Whether the service is disabled - * @since 1.28 - */ - public function isServiceDisabled( $name ) { - return isset( $this->disabled[$name] ); - } -} diff --git a/includes/services/ServiceDisabledException.php b/includes/services/ServiceDisabledException.php deleted file mode 100644 index ae15b7cea5..0000000000 --- a/includes/services/ServiceDisabledException.php +++ /dev/null @@ -1,43 +0,0 @@ -createMock( MediaWiki\Services\DestructibleService::class ); + $service = $this->createMock( Wikimedia\Services\DestructibleService::class ); $service->expects( $this->once() )->method( 'destroy' ); return $service; } @@ -248,7 +248,7 @@ class MediaWikiServicesTest extends MediaWikiTestCase { $services->defineService( 'Test', function () { - $service = $this->createMock( MediaWiki\Services\DestructibleService::class ); + $service = $this->createMock( Wikimedia\Services\DestructibleService::class ); $service->expects( $this->never() )->method( 'destroy' ); return $service; } diff --git a/tests/phpunit/includes/libs/services/ServiceContainerTest.php b/tests/phpunit/includes/libs/services/ServiceContainerTest.php new file mode 100644 index 0000000000..6674a15012 --- /dev/null +++ b/tests/phpunit/includes/libs/services/ServiceContainerTest.php @@ -0,0 +1,496 @@ +newServiceContainer(); + $names = $services->getServiceNames(); + + $this->assertInternalType( 'array', $names ); + $this->assertEmpty( $names ); + + $name = 'TestService92834576'; + $services->defineService( $name, function () { + return null; + } ); + + $names = $services->getServiceNames(); + $this->assertContains( $name, $names ); + } + + public function testHasService() { + $services = $this->newServiceContainer(); + + $name = 'TestService92834576'; + $this->assertFalse( $services->hasService( $name ) ); + + $services->defineService( $name, function () { + return null; + } ); + + $this->assertTrue( $services->hasService( $name ) ); + } + + public function testGetService() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService = new stdClass(); + $name = 'TestService92834576'; + $count = 0; + + $services->defineService( + $name, + function ( $actualLocator, $extra ) use ( $services, $theService, &$count ) { + $count++; + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( $extra, 'Foo' ); + return $theService; + } + ); + + $this->assertSame( $theService, $services->getService( $name ) ); + + $services->getService( $name ); + $this->assertSame( 1, $count, 'instantiator should be called exactly once!' ); + } + + public function testGetService_fail_unknown() { + $services = $this->newServiceContainer(); + + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->getService( $name ); + } + + public function testPeekService() { + $services = $this->newServiceContainer(); + + $services->defineService( + 'Foo', + function () { + return new stdClass(); + } + ); + + $services->defineService( + 'Bar', + function () { + return new stdClass(); + } + ); + + // trigger instantiation of Foo + $services->getService( 'Foo' ); + + $this->assertInternalType( + 'object', + $services->peekService( 'Foo' ), + 'Peek should return the service object if it had been accessed before.' + ); + + $this->assertNull( + $services->peekService( 'Bar' ), + 'Peek should return null if the service was never accessed.' + ); + } + + public function testPeekService_fail_unknown() { + $services = $this->newServiceContainer(); + + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->peekService( $name ); + } + + public function testDefineService() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function ( $actualLocator ) use ( $services, $theService ) { + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + return $theService; + } ); + + $this->assertTrue( $services->hasService( $name ) ); + $this->assertSame( $theService, $services->getService( $name ) ); + } + + public function testDefineService_fail_duplicate() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () use ( $theService ) { + return $theService; + } ); + + $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class ); + + $services->defineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testApplyWiring() { + $services = $this->newServiceContainer(); + + $wiring = [ + 'Foo' => function () { + return 'Foo!'; + }, + 'Bar' => function () { + return 'Bar!'; + }, + ]; + + $services->applyWiring( $wiring ); + + $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); + $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); + } + + public function testImportWiring() { + $services = $this->newServiceContainer(); + + $wiring = [ + 'Foo' => function () { + return 'Foo!'; + }, + 'Bar' => function () { + return 'Bar!'; + }, + 'Car' => function () { + return 'FUBAR!'; + }, + ]; + + $services->applyWiring( $wiring ); + + $services->addServiceManipulator( 'Foo', function ( $service ) { + return $service . '+X'; + } ); + + $services->addServiceManipulator( 'Car', function ( $service ) { + return $service . '+X'; + } ); + + $newServices = $this->newServiceContainer(); + + // create a service with manipulator + $newServices->defineService( 'Foo', function () { + return 'Foo!'; + } ); + + $newServices->addServiceManipulator( 'Foo', function ( $service ) { + return $service . '+Y'; + } ); + + // create a service before importing, so we can later check that + // existing service instances survive importWiring() + $newServices->defineService( 'Car', function () { + return 'Car!'; + } ); + + // force instantiation + $newServices->getService( 'Car' ); + + // Define another service, so we can later check that extra wiring + // is not lost. + $newServices->defineService( 'Xar', function () { + return 'Xar!'; + } ); + + // import wiring, but skip `Bar` + $newServices->importWiring( $services, [ 'Bar' ] ); + + $this->assertNotContains( 'Bar', $newServices->getServiceNames(), 'Skip `Bar` service' ); + $this->assertSame( 'Foo!+Y+X', $newServices->getService( 'Foo' ) ); + + // import all wiring, but preserve existing service instance + $newServices->importWiring( $services ); + + $this->assertContains( 'Bar', $newServices->getServiceNames(), 'Import all services' ); + $this->assertSame( 'Bar!', $newServices->getService( 'Bar' ) ); + $this->assertSame( 'Car!', $newServices->getService( 'Car' ), 'Use existing service instance' ); + $this->assertSame( 'Xar!', $newServices->getService( 'Xar' ), 'Predefined services are kept' ); + } + + public function testLoadWiringFiles() { + $services = $this->newServiceContainer(); + + $wiringFiles = [ + __DIR__ . '/TestWiring1.php', + __DIR__ . '/TestWiring2.php', + ]; + + $services->loadWiringFiles( $wiringFiles ); + + $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); + $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); + } + + public function testLoadWiringFiles_fail_duplicate() { + $services = $this->newServiceContainer(); + + $wiringFiles = [ + __DIR__ . '/TestWiring1.php', + __DIR__ . '/./TestWiring1.php', + ]; + + // loading the same file twice should fail, because + $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class ); + + $services->loadWiringFiles( $wiringFiles ); + } + + public function testRedefineService() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService1 = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () { + PHPUnit_Framework_Assert::fail( + 'The original instantiator function should not get called' + ); + } ); + + // redefine before instantiation + $services->redefineService( + $name, + function ( $actualLocator, $extra ) use ( $services, $theService1 ) { + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); + return $theService1; + } + ); + + // force instantiation, check result + $this->assertSame( $theService1, $services->getService( $name ) ); + } + + public function testRedefineService_disabled() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService1 = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () { + return 'Foo'; + } ); + + // disable the service. we should be able to redefine it anyway. + $services->disableService( $name ); + + $services->redefineService( $name, function () use ( $theService1 ) { + return $theService1; + } ); + + // force instantiation, check result + $this->assertSame( $theService1, $services->getService( $name ) ); + } + + public function testRedefineService_fail_undefined() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->redefineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testRedefineService_fail_in_use() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () { + return 'Foo'; + } ); + + // create the service, so it can no longer be redefined + $services->getService( $name ); + + $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class ); + + $services->redefineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testAddServiceManipulator() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService1 = new stdClass(); + $theService2 = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( + $name, + function ( $actualLocator, $extra ) use ( $services, $theService1 ) { + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); + return $theService1; + } + ); + + $services->addServiceManipulator( + $name, + function ( + $theService, $actualLocator, $extra + ) use ( + $services, $theService1, $theService2 + ) { + PHPUnit_Framework_Assert::assertSame( $theService1, $theService ); + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); + return $theService2; + } + ); + + // force instantiation, check result + $this->assertSame( $theService2, $services->getService( $name ) ); + } + + public function testAddServiceManipulator_fail_undefined() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->addServiceManipulator( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testAddServiceManipulator_fail_in_use() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () use ( $theService ) { + return $theService; + } ); + + // create the service, so it can no longer be redefined + $services->getService( $name ); + + $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class ); + + $services->addServiceManipulator( $name, function () { + return 'Foo'; + } ); + } + + public function testDisableService() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class ) + ->getMock(); + $destructible->expects( $this->once() ) + ->method( 'destroy' ); + + $services->defineService( 'Foo', function () use ( $destructible ) { + return $destructible; + } ); + $services->defineService( 'Bar', function () { + return new stdClass(); + } ); + $services->defineService( 'Qux', function () { + return new stdClass(); + } ); + + // instantiate Foo and Bar services + $services->getService( 'Foo' ); + $services->getService( 'Bar' ); + + // disable service, should call destroy() once. + $services->disableService( 'Foo' ); + + // disabled service should still be listed + $this->assertContains( 'Foo', $services->getServiceNames() ); + + // getting other services should still work + $services->getService( 'Bar' ); + + // disable non-destructible service, and not-yet-instantiated service + $services->disableService( 'Bar' ); + $services->disableService( 'Qux' ); + + $this->assertNull( $services->peekService( 'Bar' ) ); + $this->assertNull( $services->peekService( 'Qux' ) ); + + // disabled service should still be listed + $this->assertContains( 'Bar', $services->getServiceNames() ); + $this->assertContains( 'Qux', $services->getServiceNames() ); + + $this->setExpectedException( Wikimedia\Services\ServiceDisabledException::class ); + $services->getService( 'Qux' ); + } + + public function testDisableService_fail_undefined() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->redefineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testDestroy() { + $services = $this->newServiceContainer(); + + $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class ) + ->getMock(); + $destructible->expects( $this->once() ) + ->method( 'destroy' ); + + $services->defineService( 'Foo', function () use ( $destructible ) { + return $destructible; + } ); + + $services->defineService( 'Bar', function () { + return new stdClass(); + } ); + + // create the service + $services->getService( 'Foo' ); + + // destroy the container + $services->destroy(); + + $this->setExpectedException( Wikimedia\Services\ContainerDisabledException::class ); + $services->getService( 'Bar' ); + } + +} diff --git a/tests/phpunit/includes/libs/services/TestWiring1.php b/tests/phpunit/includes/libs/services/TestWiring1.php new file mode 100644 index 0000000000..b6ff4eb3b4 --- /dev/null +++ b/tests/phpunit/includes/libs/services/TestWiring1.php @@ -0,0 +1,10 @@ + function () { + return 'Foo!'; + }, +]; diff --git a/tests/phpunit/includes/libs/services/TestWiring2.php b/tests/phpunit/includes/libs/services/TestWiring2.php new file mode 100644 index 0000000000..dfff64f048 --- /dev/null +++ b/tests/phpunit/includes/libs/services/TestWiring2.php @@ -0,0 +1,10 @@ + function () { + return 'Bar!'; + }, +]; diff --git a/tests/phpunit/includes/services/ServiceContainerTest.php b/tests/phpunit/includes/services/ServiceContainerTest.php deleted file mode 100644 index aca88aa4b6..0000000000 --- a/tests/phpunit/includes/services/ServiceContainerTest.php +++ /dev/null @@ -1,498 +0,0 @@ -newServiceContainer(); - $names = $services->getServiceNames(); - - $this->assertInternalType( 'array', $names ); - $this->assertEmpty( $names ); - - $name = 'TestService92834576'; - $services->defineService( $name, function () { - return null; - } ); - - $names = $services->getServiceNames(); - $this->assertContains( $name, $names ); - } - - public function testHasService() { - $services = $this->newServiceContainer(); - - $name = 'TestService92834576'; - $this->assertFalse( $services->hasService( $name ) ); - - $services->defineService( $name, function () { - return null; - } ); - - $this->assertTrue( $services->hasService( $name ) ); - } - - public function testGetService() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService = new stdClass(); - $name = 'TestService92834576'; - $count = 0; - - $services->defineService( - $name, - function ( $actualLocator, $extra ) use ( $services, $theService, &$count ) { - $count++; - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( $extra, 'Foo' ); - return $theService; - } - ); - - $this->assertSame( $theService, $services->getService( $name ) ); - - $services->getService( $name ); - $this->assertSame( 1, $count, 'instantiator should be called exactly once!' ); - } - - public function testGetService_fail_unknown() { - $services = $this->newServiceContainer(); - - $name = 'TestService92834576'; - - $this->setExpectedException( MediaWiki\Services\NoSuchServiceException::class ); - - $services->getService( $name ); - } - - public function testPeekService() { - $services = $this->newServiceContainer(); - - $services->defineService( - 'Foo', - function () { - return new stdClass(); - } - ); - - $services->defineService( - 'Bar', - function () { - return new stdClass(); - } - ); - - // trigger instantiation of Foo - $services->getService( 'Foo' ); - - $this->assertInternalType( - 'object', - $services->peekService( 'Foo' ), - 'Peek should return the service object if it had been accessed before.' - ); - - $this->assertNull( - $services->peekService( 'Bar' ), - 'Peek should return null if the service was never accessed.' - ); - } - - public function testPeekService_fail_unknown() { - $services = $this->newServiceContainer(); - - $name = 'TestService92834576'; - - $this->setExpectedException( MediaWiki\Services\NoSuchServiceException::class ); - - $services->peekService( $name ); - } - - public function testDefineService() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function ( $actualLocator ) use ( $services, $theService ) { - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - return $theService; - } ); - - $this->assertTrue( $services->hasService( $name ) ); - $this->assertSame( $theService, $services->getService( $name ) ); - } - - public function testDefineService_fail_duplicate() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () use ( $theService ) { - return $theService; - } ); - - $this->setExpectedException( MediaWiki\Services\ServiceAlreadyDefinedException::class ); - - $services->defineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testApplyWiring() { - $services = $this->newServiceContainer(); - - $wiring = [ - 'Foo' => function () { - return 'Foo!'; - }, - 'Bar' => function () { - return 'Bar!'; - }, - ]; - - $services->applyWiring( $wiring ); - - $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); - $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); - } - - public function testImportWiring() { - $services = $this->newServiceContainer(); - - $wiring = [ - 'Foo' => function () { - return 'Foo!'; - }, - 'Bar' => function () { - return 'Bar!'; - }, - 'Car' => function () { - return 'FUBAR!'; - }, - ]; - - $services->applyWiring( $wiring ); - - $services->addServiceManipulator( 'Foo', function ( $service ) { - return $service . '+X'; - } ); - - $services->addServiceManipulator( 'Car', function ( $service ) { - return $service . '+X'; - } ); - - $newServices = $this->newServiceContainer(); - - // create a service with manipulator - $newServices->defineService( 'Foo', function () { - return 'Foo!'; - } ); - - $newServices->addServiceManipulator( 'Foo', function ( $service ) { - return $service . '+Y'; - } ); - - // create a service before importing, so we can later check that - // existing service instances survive importWiring() - $newServices->defineService( 'Car', function () { - return 'Car!'; - } ); - - // force instantiation - $newServices->getService( 'Car' ); - - // Define another service, so we can later check that extra wiring - // is not lost. - $newServices->defineService( 'Xar', function () { - return 'Xar!'; - } ); - - // import wiring, but skip `Bar` - $newServices->importWiring( $services, [ 'Bar' ] ); - - $this->assertNotContains( 'Bar', $newServices->getServiceNames(), 'Skip `Bar` service' ); - $this->assertSame( 'Foo!+Y+X', $newServices->getService( 'Foo' ) ); - - // import all wiring, but preserve existing service instance - $newServices->importWiring( $services ); - - $this->assertContains( 'Bar', $newServices->getServiceNames(), 'Import all services' ); - $this->assertSame( 'Bar!', $newServices->getService( 'Bar' ) ); - $this->assertSame( 'Car!', $newServices->getService( 'Car' ), 'Use existing service instance' ); - $this->assertSame( 'Xar!', $newServices->getService( 'Xar' ), 'Predefined services are kept' ); - } - - public function testLoadWiringFiles() { - $services = $this->newServiceContainer(); - - $wiringFiles = [ - __DIR__ . '/TestWiring1.php', - __DIR__ . '/TestWiring2.php', - ]; - - $services->loadWiringFiles( $wiringFiles ); - - $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); - $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); - } - - public function testLoadWiringFiles_fail_duplicate() { - $services = $this->newServiceContainer(); - - $wiringFiles = [ - __DIR__ . '/TestWiring1.php', - __DIR__ . '/./TestWiring1.php', - ]; - - // loading the same file twice should fail, because - $this->setExpectedException( MediaWiki\Services\ServiceAlreadyDefinedException::class ); - - $services->loadWiringFiles( $wiringFiles ); - } - - public function testRedefineService() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService1 = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () { - PHPUnit_Framework_Assert::fail( - 'The original instantiator function should not get called' - ); - } ); - - // redefine before instantiation - $services->redefineService( - $name, - function ( $actualLocator, $extra ) use ( $services, $theService1 ) { - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); - return $theService1; - } - ); - - // force instantiation, check result - $this->assertSame( $theService1, $services->getService( $name ) ); - } - - public function testRedefineService_disabled() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService1 = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () { - return 'Foo'; - } ); - - // disable the service. we should be able to redefine it anyway. - $services->disableService( $name ); - - $services->redefineService( $name, function () use ( $theService1 ) { - return $theService1; - } ); - - // force instantiation, check result - $this->assertSame( $theService1, $services->getService( $name ) ); - } - - public function testRedefineService_fail_undefined() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $this->setExpectedException( MediaWiki\Services\NoSuchServiceException::class ); - - $services->redefineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testRedefineService_fail_in_use() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () { - return 'Foo'; - } ); - - // create the service, so it can no longer be redefined - $services->getService( $name ); - - $this->setExpectedException( MediaWiki\Services\CannotReplaceActiveServiceException::class ); - - $services->redefineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testAddServiceManipulator() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService1 = new stdClass(); - $theService2 = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( - $name, - function ( $actualLocator, $extra ) use ( $services, $theService1 ) { - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); - return $theService1; - } - ); - - $services->addServiceManipulator( - $name, - function ( - $theService, $actualLocator, $extra - ) use ( - $services, $theService1, $theService2 - ) { - PHPUnit_Framework_Assert::assertSame( $theService1, $theService ); - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); - return $theService2; - } - ); - - // force instantiation, check result - $this->assertSame( $theService2, $services->getService( $name ) ); - } - - public function testAddServiceManipulator_fail_undefined() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $this->setExpectedException( MediaWiki\Services\NoSuchServiceException::class ); - - $services->addServiceManipulator( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testAddServiceManipulator_fail_in_use() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () use ( $theService ) { - return $theService; - } ); - - // create the service, so it can no longer be redefined - $services->getService( $name ); - - $this->setExpectedException( MediaWiki\Services\CannotReplaceActiveServiceException::class ); - - $services->addServiceManipulator( $name, function () { - return 'Foo'; - } ); - } - - public function testDisableService() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $destructible = $this->getMockBuilder( MediaWiki\Services\DestructibleService::class ) - ->getMock(); - $destructible->expects( $this->once() ) - ->method( 'destroy' ); - - $services->defineService( 'Foo', function () use ( $destructible ) { - return $destructible; - } ); - $services->defineService( 'Bar', function () { - return new stdClass(); - } ); - $services->defineService( 'Qux', function () { - return new stdClass(); - } ); - - // instantiate Foo and Bar services - $services->getService( 'Foo' ); - $services->getService( 'Bar' ); - - // disable service, should call destroy() once. - $services->disableService( 'Foo' ); - - // disabled service should still be listed - $this->assertContains( 'Foo', $services->getServiceNames() ); - - // getting other services should still work - $services->getService( 'Bar' ); - - // disable non-destructible service, and not-yet-instantiated service - $services->disableService( 'Bar' ); - $services->disableService( 'Qux' ); - - $this->assertNull( $services->peekService( 'Bar' ) ); - $this->assertNull( $services->peekService( 'Qux' ) ); - - // disabled service should still be listed - $this->assertContains( 'Bar', $services->getServiceNames() ); - $this->assertContains( 'Qux', $services->getServiceNames() ); - - $this->setExpectedException( MediaWiki\Services\ServiceDisabledException::class ); - $services->getService( 'Qux' ); - } - - public function testDisableService_fail_undefined() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $this->setExpectedException( MediaWiki\Services\NoSuchServiceException::class ); - - $services->redefineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testDestroy() { - $services = $this->newServiceContainer(); - - $destructible = $this->getMockBuilder( MediaWiki\Services\DestructibleService::class ) - ->getMock(); - $destructible->expects( $this->once() ) - ->method( 'destroy' ); - - $services->defineService( 'Foo', function () use ( $destructible ) { - return $destructible; - } ); - - $services->defineService( 'Bar', function () { - return new stdClass(); - } ); - - // create the service - $services->getService( 'Foo' ); - - // destroy the container - $services->destroy(); - - $this->setExpectedException( MediaWiki\Services\ContainerDisabledException::class ); - $services->getService( 'Bar' ); - } - -} diff --git a/tests/phpunit/includes/services/TestWiring1.php b/tests/phpunit/includes/services/TestWiring1.php deleted file mode 100644 index b6ff4eb3b4..0000000000 --- a/tests/phpunit/includes/services/TestWiring1.php +++ /dev/null @@ -1,10 +0,0 @@ - function () { - return 'Foo!'; - }, -]; diff --git a/tests/phpunit/includes/services/TestWiring2.php b/tests/phpunit/includes/services/TestWiring2.php deleted file mode 100644 index dfff64f048..0000000000 --- a/tests/phpunit/includes/services/TestWiring2.php +++ /dev/null @@ -1,10 +0,0 @@ - function () { - return 'Bar!'; - }, -];