Wikimedia Technical Committee/Extension point guidelines
This page is currently a draft.
More information and discussion about changes to this draft may be on the discussion page.
This guideline governs the creation of new extension points in MediaWiki. An extension point is an interface that allows an Extension to modify and extend the functionality of MediaWiki core. Since extension points act as an interface between software components that are released separately and may be maintained by different entities, they need to be stable.
In the past, extension points have often be created without much consideration for the need of stability and isolation. The purpose of this guideline is to improve that situation, by ensuring that new extension points are designed in a way that avoid breaking changes while MediaWiki evolves. This benefits extension authors as well as maintainers of MediaWiki core: extension authors get clear guarantees about how MediaWiki behaves and interacts with the extension. And MediaWiki maintainers gain flexibility to change the software's internals without having to watch out for breaking extensions.
There are several distinct types of extension points, among which the most important ones are Hooks, Services, and Handlers. The sections below provide guidance for the creation of different types of extension points.
In order to be stable, they [what?] need to be narrow and well defined, so they isolate extensions from implementation details, and help to avoid breakage when these implementation details change. This reflects the software engineering principle idea of information hiding and the Open–closed principle. To benefit from this principle, extensions MUST use only documented extension points to modify the behavior of MediaWiki.
When defining extension points of the service or handler type (see below), there are broadly two choices: providing an abstract base class for extensions to base their implementation on, or requiring extensions to directly implement a PHP interface. Defining an interface may seem like the obvious textbook solution, but it's actually rather problematic, because it provides no forward compatibility: it's impossible to add a method to an interface, or a parameter to a method in such an interface, in a way that would not break all any extension implementing it. There is no good mechanism available for making such a change backwards compatible(*).
For this reason, services and handlers type extension points MUST offer a (typically abstract) base class for extensions to base their implementations on, and MUST NOT require extensions to implement a PHP interfaces directly.
Extensions in turn MUST NOT use subclassing to modify the behavior of a class that is not defined as an extension point. Protected methods are not a stable interface per the deprecation policy, and are subject to changes without warning, unless explicitly documented to be stable or are part of a class that is a designated extension point. Similarly, the constructor signature of service objects and handler objects is not a stable interface.
(*) the textbook approach would be to introduce a new interface that extends the old one. But this forces any application logic that wants to use the new interface to do an instanceof check, or complex logic to be in place to provide automatic wrapping of implementations of the new interface.
This section is subject to the RFC "evolving the hook system", which is still under discussion as of August 2019. It will be filled in once that RFC closes.
Handlers are the primary way to add support for new specializations to MediaWiki: new API modules, new content models, new special pages, etc. A "handler" object can provide complex functionality specialized for a purpose in a more concise than what would be possible with hooks alone.
Technically speaking, handlers are objects implementing a specific interface (by subclassing, see below) in a way specialized for some purpose identified by a name. Examples of handler types include ApiModule (by module name), SpecialPage (by page name), ContentHandler (by content model), SlotRoleHandler (by slot role), REST route handlers (by route), etc.
Handlers are typically managed by a registry, which should be a service in MediaWikiServices. A registry is a type of factory that can be used to obtain the correct implementation of a given type of handler for a given name of a specialization ( an action, a type, a format, kind, flavor, etc). For instance, the SpecialPageFactory provides the correct SpecialPage object for a given page title, the SlotRoleRegistry provides the correct SlotRoleHandler implementation for a given slot role, etc.
Handler objects should behave idempotent in isolation: calling the same methods with the same parameters should always yield the same result, unless the system's state has been modified by other code in the meantime.
Each registry is responsible for one type of handler, and all handlers of a type implement a common interface (see below). There are two possible patterns for allowing extensions to register their own handler implementations with the registry:
- The registry has a method that allows new handlers to be specified, by providing an instance, an instantiator callback, or an object spec for use with ObjectFactory. The extension uses the MediaWikiServices hook to call addServiceManipulator() with a callback that calls the appropriate method on the registry.
- The registry takes a list of wiring files as a parameter, with each wiring file containing instantiators or object specifications for the handlers, to be used with a ServiceContainer or ObjectFactory. The wiring for the registry triggers a hook that extensions can use to add to the list of wiring files. Instead of files, the wiring may also be passed as an array or object.
Handlers are extension points by nature. All types of handlers should be based on an abstract base class that defines their shared interface -- a proper PHP interface may also be defined, but should never be used directly by extensions when defining their own implementations. The extension should subclass the abstract base class instead. This approach allows the interface of the Handler to change over time, new methods to be added and old methods to be deprecated, while the base class provides compatibility stubs.
The reason for using abstract base classes is to make them more future proof. If extensions were implementing PHP interfaces directly, there would be no way to change that interface. Instead, a new interface would have to be created, and all callers would need to be able to handle both, or handler objects would need to be wrapped.
In summary, the following points MUST be observed when creating a handler-style extension point:
- Handler instances MUST be managed by a registry (that is, a factory for specialized implementations of a common interface)
- The registry MUST be managed as a service in MediaWikiServices.
- The registry MUST either offer a method for registering new handlers by supplying a callback or object spec for use with ObjectFactory, or the wiring that instantiates the registry MUST injects the specs for all handlers into the registry, after triggering a hook that allows extensions to add to the list of known handlers.
- A (typically abstract) base class for handler implementations MUST be defined. Extensions MUST NOT be required to implement an PHP interface directly.
Service objects are (pseudo)singletons managed by a ServiceContainer (typically MediaWikiServices). They provide abstractions for all essential functionality, encoding storage logic, application logic, etc.
Service objects should be (pseudo)stateless, or more specifically, behave idempotent in isolation: calling the same methods with the same parameters should always yield the same result, unless the system's state has been modified by other code in the meantime.
Some services may be declared to be extension points that can be replaced or wrapped by extensions. An example of a service than can be replaced entirely is the MainObjectStash. Only services that are explicitly declared as extension points should be wrapped or replaced.
Extensions can replace, wrap, or manipulate a service by registering a handler to the MediaWikiServices hook which gets triggered when all wiring has been defined. In the hook handler, the extension can then register a callback with manipulateService(), to manipulate, wrap, or replace services. Some services can also be replaced via configuration settings.
In summary, the following points MUST be observed when creating a service-style extension point:
- The service MUST be clearly documented to act as an extension point.
- An (abstract) base class MUST be provided for extensions to extend with their own implementation.
Other extension points
Other extension points should follow the same ideas outlined above: narrow interfaces exposing only what is necessary, and isolating extensions from implementation details. This document may be amended in the future to provide more specific guidance for additional types of extension points, such as skins, resource loader modules, and maintenance scripts.