Problem
Instances of SpecialPage and ApiBase need to be able to use services from the container. However, there is currently not a way to inject services into these classes without making the method aware of the container. This makes testing difficult as the classes have to be tested with the container rather than mocked dependencies. This does not mean that SpecialPages and API modules need to become services themselves (and thus be a part of the container).
Proposals
Solution #1: Inject Container into Factory Closure
SpecialPageFactory::getPage() allows a Special Pages's definition to be a callable:
if ( is_callable( $rec ) ) { // Use callback to instantiate the special page $page = $rec(); }
we could change this to inject the container in the callable:
if ( is_callable( $rec ) ) { // Use callback to instantiate the special page $page = $rec( MediaWikiServices::getInstance() ); }
this would allow SpecialPages to be instantiated with an anonymous function (much like includes/ServiceWiring.php). SpecialPageFactory::$coreList would become a non-static and the SpecialPages with dependencies would be closures rather than strings.
An implementation would look like:
'EditTags' => function( ServiceContainer $container ) { return new \SpecialEditTags( $container->get( 'PermissionManager') ); }
Likewise in ApiModuleManager::instantiateModule allows a factory key to be defined as a callable:
if ( $factory !== null ) { // create instance from factory $instance = call_user_func( $factory, $this->mParent, $name ); if ( !$instance instanceof $class ) { throw new MWException( "The factory function for module $name did not return an instance of $class!" ); } }
we could change this to inject the container into the factory:
if ( $factory !== null ) { // create instance from factory $instance = call_user_func( $factory, $this->mParent, $name, MediaWikiServices::getInstance() ); if ( !$instance instanceof $class ) { throw new MWException( "The factory function for module $name did not return an instance of $class!" ); } }
again, ApiMain::$Modules would become non-static and an implementation could look like this:
'revisiondelete' => [ 'class' => ApiRevisionDelete::class, 'factory' => function ( ApiMain $main, $name, ServiceContainer $container ) : ApiRevisionDelete { return new ApiRevisionDelete( $main, $name, '', $container->get( 'PermissionManager' ) ); }, ],
Solution #2: Implement an interface with a static method for injecting the container to create the object
A new interface for SpecialPage could be created like this:
interface ContainerInjectionInterface { public static function create( ServiceContainer $container ); }
then SpecialPageFactory::getPage() can be updated from this:
elseif ( is_string( $rec ) ) { $className = $rec; $page = new $className; }
to something like this:
elseif ( is_string( $rec ) ) { $className = $rec; if ( is_subclass_of( $className, ContainerInjectionInterface::class ) ) { $page = $className::create( MediaWikiServices::getInstance() ); } else { $page = new $className; } }
an implementation might look like this:
class SpecialEditTags extends UnlistedSpecialPage implements ContainerInjectionInterface { public static function create( ServiceContainer $container ) { return new self( $container->get( 'PermissionManager' ) ); } }
likewise, a new interface would be created for API modules:
interface ApiContainerInjectionInterface { public static function create( ServiceContainer $container, ApiMain $mainModule, $moduleName ); }
and ApiModuleManager::instantiateModule() would be updated from this:
// create instance from class name $instance = new $class( $this->mParent, $name );
to this:
// create instance from class name if ( is_subclass_of( $class, ApiContainerInjectionInterface::class ) ) { $instance = $class::create( MediaWikiServices::getInstance(), $this->mParent, $name ); } else { $instance = new $class( $this->mParent, $name ); }
and an implementation might look like this:
class ApiRevisionDelete extends ApiBase implements ApiContainerInjectionInterface { public static function create( ServiceContainer $container, ApiMain $mainModule, $moduleName ) { return new self( $mainModule, $moduleName, '', $container->get( 'PermissionManager' ) ); } }
Solution #3: Allow Special Pages and API Modules to be services in the service container.
Instead of including the service wiring in SpecialPageFactory or ApiModuleManager instead add the service wiring to includes/ServiceWiring.php like any other service. Then the Special Page or API Module configuration would accept a reference to a service (that implements an interface?) rather than a reference to a class.
Chosen solution: Utilize ObjectFactory
With T222409: Standardize declarative object construction in MediaWiki, ObjectFactory now supports instantiating objects from a spec with services specified in the spec. It is now a service of its own, which allows it to be injected in any class that needs to construct objects that need services.
This transforms constructing code from
if ( is_callable( $rec ) ) { $page = $rec(); } elseif ( is_string( $rec ) ) { $page = new $rec; }
to:
$page = $this->objectFactory->createObject( $rec, [ 'allowClassName' => true, 'allowCallable' => true ] );
The spec notes which services are needed:
$rec = [ 'class' => SpecialAllPages::class, 'services' => [ 'PagerFactory' ] ];
ApiModuleManager::instantiateModule now takes the previously supported spec syntax for ApiModuleManager::addModules and applies it to ApiModuleManager::addModule, replacing the $class and $factory parameters with a single one that fulfills the function of both through the ObjectFactory spec.