mediawiki

GitHub

🌻 The collaborative editing software that runs Wikipedia. Mirror from https://gerrit.wikimedia.org/g/mediawiki/core. See https://mediawiki.org/wiki/Developer_access for contributing.

RAW Doc

Events

Domain Events {#events}
=====

Introduction

Domain Events are a way for MediaWiki core components and extensions to be
informed about events that have occurred in another component or extension by
registering listeners. Domain Events in MediaWiki are similar to hooks, but
offer more flexibility in data modeling and stronger guarantees around the
invocation of listener code. They are introduced with MediaWiki 1.44 to improve
the developer experience by making use of an established design pattern that is
easy to use and helps reduce coupling, making the codebase more sustainable.

The idea of domain event in MediaWiki is inspired by domain driven design where
the term is used to describe something that happened in the past that users care
about, typically something that changes the visible state of the system. Martin
Fowler writes: "the essence of a Domain Event is that you use it to capture
things that can trigger a change to the state of the application you are
developing".

For more information see the documentation on mediawiki.org:
https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Domain_events

Motivation

The Domain Event System is intended to enable event oriented processing within
and eventually around MediaWiki. Events become first-class concepts for
connecting MediaWiki core components and extensions as well as integrating
MediaWiki with other services in a distributed system.

Domain Events are designed to replace a certain type of hook as an extension
interface, and to provide a mechanism for decoupling core components using the
observer pattern. Eventually, MediaWiki should become able to broadcast events
to and receive events from other wikis and other services.

The overall motivation for introducing domain events is to make MediaWiki and
extension development more sustainable:

* Improve component boundaries between core components by applying the observer
pattern (aka listener pattern). Listeners remove the need for code that
affects a change to know about all code that needs to be informed about it.
* Clarify the semantics of extension callbacks invoked as a result of a change,
particularly with respect to transactional context. Standardize deferred
update behavior, which will reduce boilerplate code and risk of
misimplementation.
* Make the extension interface more future proof by avoiding the rigidity
imposed by using PHP interfaces to define hook parameters. Due to limitations
of PHP, method signatures defined by extensions can’t be modified in a
backwards-compatible way.
* Prepare for the creation of a generic relay mechanism for broadcasting events
over an event bus. Broadcasting itself is not in scope for the initial phase,
but accommodating that use case is a design goal.

The design follows the idea of domain events as defined in domain driven design:
events represent changes maintained by a given component (or bounded
context).

---

Hooks

Hooks
=====

Introduction

Hooks allow MediaWiki Core to call extensions or allow one extension to call
another extension. For more information and a list of hooks, see
https://www.mediawiki.org/wiki/Manual:Hooks

Starting in MediaWiki 1.35, each hook called by MediaWiki Core has an
associated interface with a single method. To call the hook, obtain a "hook
runner" object, which implements the relevant interface, and call the relevant
method. To handle a hook event in an extension, create a handler object which
implements the interface.

The name of the interface is the name of the hook with "Hook" added to the end.
Interfaces are typically placed in the namespace of their primary caller.

The method name for the hook is the name of the hook, prefixed with "on".

Several hooks had colons in their name, which are invalid in an interface or
method name. These hooks have interfaces and method names in which the colons
are replaced with underscores.

For example, if the hook is called Mash, we might have the interface:

interface MashHook {
public function onMash( $banana );
}

Hooks can be defined and called by extensions. The extension should define a
hook interface for each hook, as described above.

HookContainer

HookContainer is a service which is responsible for maintaining a list of hook
handlers and calling those handlers when requested. HookContainer is not aware
of hook interfaces or parameter types.

HookContainer provides hook metadata. For example, isRegistered() tells us
whether there are any handlers for a given hook event.

A HookContainer instance can be obtained from the global service locator with
MediaWikiServices::getHookContainer(). When implementing a service that needs
to call a hook, a HookContainer object should be passed to the constructor of
the service.

Hook runner classes

A hook runner is a class which implements hook interfaces, proxying the calls
to HookContainer::run().

MediaWiki has two hook runner classes: HookRunner and ApiHookRunner.
ApiHookRunner has proxy methods for all hooks which are called by the Action
API. HookRunner has proxy methods for all hooks which are called by other parts
of Core. Some hooks are implemented in both classes.

Extensions which call hooks should create their own hook runner class, by
analogy with the ones in Core. Hook runner classes are effectively internal
to the module which calls the relevant hooks. Reorganisation of the hook
calling code may lead to methods being removed from hook runner classes. Thus,
it is safer for extensions to define their own hook runner classes even if
they are calling Core hooks.

New code should typically be written in a service which takes a HookContainer
as a constructor parameter. However, for the convenience of existing static
functions in MediaWiki Core, Hooks::runner() may be used to obtain a
HookRunner instance. This is equivalent to

new HookRunner( MediaWikiServices::getInstance()->getHookContainer() )

For example, to call the hook Mash, as defined above, in static code:

Hooks::runner()->onMash( $banana );

How to handle a hook event in an extension

In extension.json, there is a new attribute called HookHandlers. This is
an object mapping the handler name to an ObjectFactory specification describing
how to create the handler object. The specification will typically have a
class member with the name of the handler class. For example, in an extension
called FoodProcessor, we may have:

"HookHandlers": {
"main": {
"class": "MediaWiki\\Extension\\FoodProcessor\\HookHandler"
}
}

Then in the Hooks attribute, instead of a function name, the value will be the
handler name:

"Hooks": {
"Mash": "main"
}

Or more explicitly, by using an object instead of a string for the handler:

"Hooks": {
"Mash": {
"handler": "main"
}
}

Note that while your HookHandler class will implement an interface that ends
with the word "Hook", in extension.json you should omit the word "Hook"
from the key in the Hooks definition. For example, in the definitions above,
the key must be "Mash", not "MashHook".

Then the extension will define a handler class:

namespace MediaWiki\Extension\FoodProcessor;

class HookHandler implements MashHook {
public function onMash( $banana ) {
// Implementation goes here
}
}

Service dependencies

The ObjectFactory specification in HookHandlers can contain a list of services
which should be instantiated and provided to the constructor or factory
function for the handler. For example:

"HookHandlers": {
"main": {
"class": "MediaWiki\\Extension\\FoodProcessor\\HookHandler",
"services": [ "ReadOnlyMode" ]
}
}

However, care should be taken with this feature. Some services have expensive
constructors, so requesting them when handling commonly-called hooks may damage
performance. Also, some services may not be safe to construct from within a hook
call.

The safest pattern for service injection is to use a separate handler for each
hook, and to inject only the services needed by that hook.

Calling a hook with the noServices option disables service injection. If a
handler for such a hook specifies services, an exception will be thrown when
the hook is called.

Returning and aborting

If a hook handler returns false, HookContainer will stop iterating through the
list of handlers and will immediately return false.

If a hook handler returns true, or if there is no return value (causing it to
effectively return null), then HookContainer will continue to call any other
remaining handlers. Eventually HookContainer::run() will return true.

If there were no registered handlers, HookContainer::run() will return true.

Some hooks are declared to be "not abortable". If a handler for a non-abortable
hook returns false, an exception will be thrown. A hook is declared to be not
abortable by passing [ "abortable" => false ] in the $options parameter to
HookContainer::run().

Aborting is properly used to enforce a convention that only one extension
may handle a given hook call.

Aborting is sometimes used as a generic return value, to indicate that the
caller should stop performing some action.

Most hook callers do not check the return value from HookContainer::run() and
there is no real concept of aborting. The only effect of returning false from
a handler of these hooks is to break other extensions.

Theoretically, extensions which are registered first in LocalSettings.php will
be called first, and thus will have the first opportunity to abort a hook call.
This behaviour should not be relied upon. In the new hook system, handlers
registered in the legacy way are called first, before handlers registered in
the new way.

Parameters passed by reference

The typical way for a handler to return data to the caller is by modifying a
parameter which was passed by reference. This is sometimes called "replacement".

Reference parameters were somewhat overused in early versions of MediaWiki. You
may find that some parameters passed by reference cannot reasonably be modified.
Replacement either has no effect on the caller or would cause unexpected or
inconsistent effects. Handlers should generally only replace a parameter when it
is clear from the documentation that replacement is expected.

How to define a new hook

* Create a hook interface, typically in a subnamespace called Hook relative
to the caller namespace. For example, if the caller is in a namespace called
MediaWiki\Foo, the hook interface might be placed in MediaWiki\Foo\Hook.
* Add an implementation to the relevant HookRunner class.

Hook deprecation

Core hooks are deprecated by adding them to an array in the DeprecatedHooks
class. Hooks declared in extensions may be deprecated by listing them in the
DeprecatedHooks attribute:

"DeprecatedHooks": {
"Mash": {
"deprecatedVersion": "2.0",
"component": "FoodProcessor"
}
}

If the component is not specified, it defaults to the name of the extension.

The hook interface should be marked as deprecated by adding @deprecated to the
interface doc comment. The interface doc comment is a better place for
@deprecated than the method doc comment, because this causes the interface to
be deprecated for implementation. Deprecating the method only causes calling
to be deprecated, not handling.

Deprecating a hook in this way activates a migration system called
call filtering. Extensions opt in to call filtering of deprecated hooks by
acknowledging deprecation. An extension acknowledges deprecation with the
deprecated parameter in the Hooks attribute:

"Hooks": {
"Mash": {
"handler": "main",
"deprecated": true
}
}

If deprecation is acknowledged by the extension:

* If MediaWiki knows that the hook is deprecated, the handler will not be
called. The call to the handler is filtered.
* If MediaWiki does not have the hook in its list of deprecated hooks, the
handler will be called anyway.

Deprecation acknowledgement is a way for the extension to say that it has made
some other arrangement for implementing the relevant functionality and does
not need the handler for the deprecated hook to be called.

Call filtering example

Suppose the hook Mash is deprecated in MediaWiki 2.0, and is replaced by a
new one called Slice. In our example extension FoodProcessor 1.0, the
Mash hook is handled. In FoodProcessor 2.0, both Mash and Slice have
handlers, but deprecation of Mash is acknowledged. Thus:

* With MediaWiki 2.0 and FoodProcessor 1.0, onMash is called but raises a
deprecation warning.
* With MediaWiki 2.0 and FoodProcessor 2.0, onMash is filtered, and onSlice
is called.
* With MediaWiki 1.0 and FoodProcessor 2.0, onMash is called, since it is not
yet deprecated in Core. onSlice is not called since it does not yet exist
in Core.

So the call filtering system provides both forwards and backwards compatibility.

Silent deprecation

Developers sometimes use two stages of deprecation: "soft" deprecation in which
the deprecated entity is merely discouraged in documentation, and "hard"
deprecation in which a warning is raised. When you soft-deprecate a hook, it is
important to register it as deprecated so that call filtering is activated.
Activating call filtering simplifies the task of migrating extensions to the
new hook.

To deprecate a hook without raising deprecation warnings, use the "silent" flag:

"DeprecatedHooks": {
"Mash": {
"deprecatedVersion": "2.0",
"component": "FoodProcessor",
"silent": true
}
}

As with hard deprecation, @deprecated should be added to the interface.

---

Injection

Dependency Injection {#dependencyinjection}
=======

This is an overview of how MediaWiki uses of dependency injection.
The design originates from RFC T384.

The term "dependency injection" (DI) refers to a pattern in object oriented
programming. DI tries to improve modularity by reducing strong coupling
between classes. In practical terms, this means that anything an object needs
to operate should be injected from the outside. The object itself should only
know narrow interfaces, no concrete implementation of the logic it relies on.

The requirement to inject everything typically results in an architecture based
on two main kinds of objects: simple "value" objects with no business logic
(and often immutable), and essentially stateless "service" objects that use
other service objects to operate on value objects.

As of 2022 (MediaWiki 1.39), MediaWiki has adopted dependency injection in much
of its code. However, some operations still require the use of singletons or
otherwise involve global state.

Overview

The heart of the DI in MediaWiki is the central service locator,
MediaWikiServices, which acts as the top-level factory (or registry) for
services. MediaWikiServices represents the tree (or network) of service objects
that define MediaWiki's application logic. It acts as an entry point to all
dependency injection for MediaWiki core.

When MediaWikiServices::getInstance() is first called, it will create an
instance of MediaWikiServices and populate it with the services defined by
MediaWiki core in includes/ServiceWiring.php, as well as any additional
bootstrapping files specified in $wgServiceWiringFiles. The service
wiring files define the (default) service implementations to use, and
specifies how they depend on each other ("wiring").

Extensions can add their own wiring files to $wgServiceWiringFiles, in order
to define their own service. Extensions may also use the MediaWikiServices
hook to replace ("redefine") a core service, by calling methods on the
MediaWikiServices instance.

It should be noted that the term "service locator" is often used to refer to a
top-level factory that is accessed directly, throughout the code, to avoid
explicit dependency injection. In contrast, the term "DI container" is often
used to describe a top-level factory that is only accessed only inside service
wiring code when instantiating service classes. We use the term "service locator"
because it is more descriptive than "DI container", even though application
logic is strongly discouraged from accessing MediaWikiServices directly.

MediaWikiServices::getInstance() should ideally be accessed only in "static
entry points" such as hook handler functions. See "Migration" below.

Principles {#di-principles}

Service classes generally only vary on site configuration and are
deterministic and agnostic of global state. It is the responsibility of
callers to a service object to obtain and derive information from a
web request (such as title, user, language, WebRequest, RequestContext),
and pass this to specific methods of a service class as-needed. See
T218555 for related discussion.

Consider using the factory pattern if your service would otherwise be
unergonomic or slow, e.g. due to passing many parameters and/or recomputing
the same derived information. This keeps the global state out of the
service class, by having the service be a factory from which the caller
can obtain a (re-usable) object for its specific context.

This design ensures service classes are safe to use in both user-facing
contexts on the web (e.g. index.php page views and special pages), as
well as in an API, job, or maintenance script. It also ensures that
within a web-facing context the same service can be safely used
multiple times to perform different operations, without incorrectly
implying certain commonalities between these calls. Lastly, this
restriction allows services to be instantiated across wikis in the
future.

If a feature is not ready to meet these requirements, keep it outside
the service container. This avoids false confidence in the safety of an
injected service, and its ripple effect on other services.

Principle exemption

There is a limited exemption to the above principles for "inconsequential
state". That is, global state may be used directly if and only if used
for diagnostics or to optimise performance, so long as they do not
change the observed functional outcome of a called method.

Examples of safe and inconsequential state:


* Use $_SERVER['REQUEST_TIME_FLOAT'] or ConvertibleTimestamp::now
to help compute a time measure that is sent to a metric service.

* Use wfHostname(), PHP_SAPI, or WikiMap::getCurrentWikiId()
to describe where, how, or for which wiki the overall process was
created and send it as message context to a logging service.

* Use WebRequest::getRequestId() to automatically inject a
header into HTTP requests to other services. These are for tracking
purposes only.

* Use function_exists('apcu_fetch') to automatically enable use
of caching.

Examples of unsafe state in a service class:

* Do not use WikiMap::getCurrentWikiId() as the default value
to obtain a database connection.

* Do not use $_SERVER['SERVER_NAME'] to inject a header into
HTTP requests to other services to control which wiki to operate on.

Create a new service

To create a new service in MediaWiki core, write a function that will return
the appropriate class instantiation for that service in ServiceWiring.php. This
makes the service available through the generic getService() method on the
MediaWikiServices class. We then also add a wrapper method to
MediaWikiServices.php with a discoverable method named and strictly typed
return value to reduce mistakes and improve static analysis.

Service Reset

Services get their configuration injected, and changes to global
configuration variables will not have any effect on services that were already
instantiated. This would typically be the case for low level services like
the ConfigFactory or the ObjectCacheManager, which are used during extension
registration. To address this issue, Setup.php resets the global service
locator instance by calling MediaWikiServices::resetGlobalInstance() once
configuration and extension registration is complete.

Note that "unmanaged" legacy services services that manage their own singleton
must not keep references to services managed by MediaWikiServices, to allow a
clean reset. After the global MediaWikiServices instance got reset, any such
references would be stale, and using a stale service will result in an error.

Services should either have all dependencies injected and be themselves managed
by MediaWikiServices, or they should use the Service Locator pattern, accessing
service instances via the global MediaWikiServices instance state when needed.
This ensures that no stale service references remain after a reset.

Configuration

When the default MediaWikiServices instance is created, a Config object is
provided to the constructor. This Config object represents the "bootstrap"
configuration which will become available as the 'BootstrapConfig' service.
As of MW 1.27, the bootstrap config is a GlobalVarConfig object providing
access to the $wgXxx configuration variables.

The bootstrap config is then used to construct a 'ConfigFactory' service,
which in turn is used to construct the 'MainConfig' service. Application
logic should use the 'MainConfig' service (or a more specific configuration
object). 'BootstrapConfig' should only be used for bootstrapping basic
services that are needed to load the 'MainConfig'.

Note: Several well known services in MediaWiki core act as factories
themselves, e.g. ApiModuleManager, ObjectCache, SpecialPageFactory, etc.
The registries these factories are based on are currently managed as part of
the configuration. This may however change in the future.

Migration

This section provides some recipes for improving code modularity by reducing
strong coupling. The dependency injection mechanism described above is an
essential tool in this effort.

Migrate access to global service instances and config variables


Assume Foo is a class that uses the $wgScriptPath global and calls
wfGetDB() to get a database connection, in non-static methods.
* Add $scriptPath as a constructor parameter and use $this->scriptPath
instead of $wgScriptPath.
* Add IConnectionProvider $dbProvider as a constructor parameter. Use
$this->dbProvider->getReplicaDatabase() instead of wfGetDB( DB_REPLICA ),
$this->dbProvider->->getPrimaryDatabase() instead of wfGetDB( DB_PRIMARY ).
* Any code that calls Foo's constructor would now need to provide the
$scriptPath and $dbProvider. To avoid this, avoid direct instantiation
of services all together - see below.

Migrate services with multiple configuration variables


When a service needs multiple configuration globals injected, a ServiceOptions
object is commonly used with the service class defining a public constant
(usually CONSTRUCTOR_OPTIONS) with an array of settings that the class needs
access to.

php
<?php

class DemoService {

public const CONSTRUCTOR_OPTIONS = [
'Foo',
'Bar',
];

public function __construct( private readonly ServiceOptions $options ) {
// ServiceOptions::assertRequiredOptions ensures that all of the
// settings listed in CONSTRUCTOR_OPTIONS are available
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
// $wgFoo is now available with $this->options->get( 'Foo' )
// $wgBar is now available with $this->options->get( 'Bar' )
}

}

ServiceOptions objects are constructed within ServiceWiring.php and can also
be created in tests.

php
'DemoService' => static function ( MediaWikiServices $services ): DemoService {
return new DemoService(
new ServiceOptions(
DemoService::CONSTRUCTOR_OPTIONS,
$services->getMainConfig()
),
);
},

Migrate class-level singleton getters


Assume class Foo has mostly non-static methods, and provides a static
getInstance() method that returns a singleton (or default instance).
* Add an instantiator function for Foo into ServiceWiring.php. The
instantiator would do exactly what Foo::getInstance() did. However, it
should replace any access to global state with calls to $services->getXxx()
to get a service, or $services->getMainConfig()->get() to get a
configuration setting.
* Add a getFoo() method to MediaWikiServices. Don't forget to add the
appropriate test cases in MediaWikiServicesTest.
* Turn Foo::getInstance() into a deprecated alias for
MediaWikiServices::getInstance()->getFoo(). Change all calls to
Foo::getInstance() to use injection (see above).

Migrate direct service instantiation


Assume class Bar calls new Foo().
* Add an instantiator function for Foo into ServiceWiring.php and add a
getFoo() method to MediaWikiServices. Don't forget to add the appropriate
test cases in MediaWikiServicesTest.
* In the instantiator, replace any access to global state with calls
to $services->getXxx() to get a service, or
$services->getMainConfig()->get() to get a configuration setting.
* The code in Bar that calls Foo's constructor should be changed to have a
Foo instance injected; Eventually, the only code that instantiates Foo is
the instantiator in ServiceWiring.php.
* As an intermediate step, Bar's constructor could initialize the $foo
member variable by calling MediaWikiServices::getInstance()->getFoo(). This
is acceptable as a stepping stone, but should be replaced by proper injection
via a constructor argument. Do not however inject the MediaWikiServices
object!

Migrate parameterized helper instantiation


Assume class Bar creates some helper object by calling new Foo( $x ),
and Foo uses a global singleton of the Xyzzy service.
* Define a FooFactory class (or a FooFactory interface along with a
MyFooFactory implementation). FooFactory defines the method
newFoo( $x ) or getFoo( $x ), depending on the desired semantics (newFoo
would guarantee a fresh instance). When Foo gets refactored to have Xyzzy
injected, FooFactory will need a Xyzzy instance, so newFoo() can pass it
to new Foo().
* Add an instantiator function for FooFactory into ServiceWiring.php and add a
getFooFactory() method to MediaWikiServices. Don't forget to add the
appropriate test cases in MediaWikiServicesTest.
* The code in Bar that calls Foo's constructor should be changed to have a
FooFactory instance injected; Eventually, the only code that instantiates
Foo are implementations of FooFactory, and the only code that instantiates
FooFactory is the instantiator in ServiceWiring.php.
* As an intermediate step, Bar's constructor could initialize the $fooFactory
member variable by calling MediaWikiServices::getInstance()->getFooFactory().
This is acceptable as a stepping stone, but should be replaced by proper
injection via a constructor argument. Do not however inject the
MediaWikiServices object!

Migrate a handler registry


Assume class Bar calls FooRegistry::getFoo( $x ) to get a specialized Foo
instance for handling $x.
* Turn getFoo into a non-static method.
* Add an instantiator function for FooRegistry into ServiceWiring.php and add
a getFooRegistry() method to MediaWikiServices. Don't forget to add the
appropriate test cases in MediaWikiServicesTest.
* Change all code that calls FooRegistry::getFoo() statically to call this
method on a FooRegistry instance. That is, Bar would have a $fooRegistry
member, initialized from a constructor parameter.
* As an intermediate step, Bar's constructor could initialize the $fooRegistry
member variable by calling
MediaWikiServices::getInstance()->getFooRegistry(). This is acceptable as a
stepping stone, but should be replaced by proper injection via a constructor
argument. Do not however inject the MediaWikiServices object!

Migrate deferred service instantiation


Assume class Bar calls new Foo(), but only when needed, to avoid the cost of
instantiating Foo().
* Define a FooFactory interface and a MyFooFactory implementation of that
interface. FooFactory defines the method getFoo() with no parameters.
* Precede as for the "parameterized helper instantiation" case described above.

Migrate a class with only static methods


Assume Foo is a class with only static methods, such as frob(), which
interacts with global state or system resources.
* Introduce a FooService interface and a DefaultFoo implementation of that
interface. FooService contains the public methods defined by Foo.
* Add an instantiator function for FooService into ServiceWiring.php and
add a getFooService() method to MediaWikiServices. Don't forget to
add the appropriate test cases in MediaWikiServicesTest.
* Add a private static getFooService() method to Foo. That method just
calls MediaWikiServices::getInstance()->getFooService().
* Make all methods in Foo delegate to the FooService returned by
getFooService(). That is, Foo::frob() would do
self::getFooService()->frob().
* Deprecate Foo. Inject a FooService into all code that calls methods
on Foo, and change any calls to static methods in foo to the methods
provided by the FooService interface.

Migrate static hook handler functions (to allow unit testing)


Assume MyExtHooks::onFoo is a static hook handler function that is called with
the parameter $x; Further assume MyExt::onFoo needs service Bar, which is
already known to MediaWikiServices (if not, see above).
* Create a non-static doFoo( $x ) method in MyExtHooks that has the same
signature as onFoo( $x ). Move the code from onFoo() into doFoo(),
replacing any access to global or static variables with access to instance
member variables.
* Add a constructor to MyExtHooks that takes a Bar service as a parameter.
* Add a static method called newFromGlobalState() with no parameters. It
should just return
new MyExtHooks( MediaWikiServices::getInstance()->getBar() ).
* The original static handler method onFoo( $x ) is then implemented as
self::newFromGlobalState()->doFoo( $x ).

Migrate a "smart record"


Assume Thingy is a "smart record" that "knows" how to load and store itself.
For this purpose, Thingy uses wfGetDB().
* Create a "dumb" value class ThingyRecord that contains all the information
that Thingy represents (e.g. the information from a database row). The value
object should not know about any service.
* Create a DAO-style service for loading and storing ThingyRecords, called
ThingyStore. It may be useful to split the interfaces for reading and
writing, with a single class implementing both interfaces, so we in the
end have the ThingyLookup and ThingyStore interfaces, and a SqlThingyStore
implementation.
* Add instantiator functions for ThingyLookup and ThingyStore in
ServiceWiring.php. Since we want to use the same instance for both service
interfaces, the instantiator for ThingyLookup would return
$services->getThingyStore().
* Add getThingyLookup() and getThingyStore() methods to MediaWikiServices.
Don't forget to add the appropriate test cases in MediaWikiServicesTest.
* In the old Thingy class, replace all member variables that represent the
record's data with a single ThingyRecord object.
* In the old Thingy class, replace all calls to static methods or functions,
such as wfGetDB(), with calls to the appropriate services, such as
IConnectionProvider::getReplicaDatabase().
* In Thingy's constructor, pull in any services needed, such as the
IConnectionProvider, by using MediaWikiServices::getInstance(). These services
cannot be injected without changing the constructor signature, which
is often impractical for "smart records" that get instantiated directly
in many places in the code base.
* Deprecate the old Thingy class. Replace all usages of it with one of the
three new classes: loading needs a ThingyLookup, storing needs a
ThingyStore, and reading data needs a ThingyRecord.

Migrate lazy loading


Assume Thingy is a "smart record" as described above, but requires lazy
loading of some or all the data it represents.
* Instead of a plain object, define ThingyRecord to be an interface. Provide a
"simple" and "lazy" implementations, called SimpleThingyRecord and
LazyThingyRecord. LazyThingyRecord knows about some lower level storage
interface, like a LoadBalancer, and uses it to load information on demand.
* Any direct instantiation of a ThingyRecord would use the
SimpleThingyRecord implementation.
* SqlThingyStore however creates instances of LazyThingyRecord, and injects
whatever storage layer service LazyThingyRecord needs to perform lazy
loading.

---

Introduction

Introduction {#mainpage}
===============================

Welcome to the MediaWiki autogenerated documentation system.
If you are looking to use, install or configure your wiki, see the main site: <https://www.mediawiki.org/>.

Helpful resources
-------------------------------
- General information
- Installation guide
- Configuration
- New Developers

---

Language

Language
=======

The Language object handles all readable text produced by the software.

See MediaWiki.org
for documentation relating to using localized messages.

---

LinkCache

The LinkCache class maintains a list of article titles and the information about
whether or not the article exists in the database. This is used to mark up links
when displaying a page. If the same link appears more than once on any page,
then it only has to be looked up once. In most cases, link lookups are done in
batches with the LinkBatch class, or the equivalent in Parser::replaceLinkHolders(),
so the link cache is mostly useful for short snippets of parsed text (such as
the site notice), and for links in the navigation areas of the skin.

The link cache was formerly used to track links used in a document for the
purposes of updating the link tables. This application is now deprecated.

To create a batch, you can use the following code:

~~~{.php}
$pages = [ 'Main Page', 'Project:Help', / ... / ];
$titles = [];

foreach( $pages as $page ){
$titles[] = Title::newFromText( $page );
}

$linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
$batch = $linkBatchFactory->newLinkBatch( $titles );
$batch->execute();
~~~

---

Logger

MediaWiki.Logger.LoggerFactory implements a [PSR-3] compatible message logging
system.

Named Psr.Log.LoggerInterface instances can be obtained from the
MediaWiki.Logger.LoggerFactory::getInstance() static method.
MediaWiki.Logger.LoggerFactory expects a class implementing the
MediaWiki.Logger.Spi interface to act as a factory for new
Psr.Log.LoggerInterface instances.

The "Spi" in MediaWiki.Logger.Spi stands for "service provider interface". An
SPI is an API intended to be implemented or extended by a third party. This
software design pattern is intended to enable framework extension and
replaceable components. It is specifically used in the
MediaWiki.Logger.LoggerFactory service to allow alternate PSR-3 logging
implementations to be easily integrated with MediaWiki.

The service provider interface allows the backend logging library to be
implemented in multiple ways. The $wgMWLoggerDefaultSpi global provides the
classname of the default MediaWiki.Logger.Spi implementation to be loaded at
runtime. This can either be the name of a class implementing the
MediaWiki.Logger.Spi with a zero argument constructor or a callable that will
return an MediaWiki.Logger.Spi instance. Alternately the
MediaWiki.Logger.LoggerFactory::registerProvider() static method can be called
to inject an MediaWiki.Logger.Spi instance into the LoggerFactory and bypass the
use of the default configuration variable.

The MediaWiki.Logger.LegacySpi class implements a service provider to generate
MediaWiki.Logger.LegacyLogger instances. The MediaWiki.Logger.LegacyLogger class
implements the PSR-3 logger interface and provides output and configuration
equivalent to the historic logging output of wfDebug, wfDebugLog, wfLogDBError
and wfErrorLog. The MediaWiki.Logger.LegacySpi class is the default service
provider configured in DefaultSettings.php. It's usage should be transparent for
users who are not ready or do not wish to switch to a alternate logging
platform.

The MediaWiki.Logger.MonologSpi class implements a service provider to generate
Psr.Log.LoggerInterface instances that use the [Monolog] logging library. See
the PHP docs (or source) for MediaWiki.Logger.MonologSpi for details on the
configuration of this provider. The default configuration installs a null
handler that will silently discard all logging events. The documentation
provided by the class describes a more feature rich logging configuration.

Classes


* MediaWiki.Logger.LoggerFactory: Factory for Psr.Log.LoggerInterface loggers
* MediaWiki.Logger.Spi: Service provider interface for
MediaWiki.Logger.LoggerFactory
* MediaWiki.Logger.NullSpi: MediaWiki.Logger.Spi for creating instances that
discard all log events
* MediaWiki.Logger.LegacySpi: Service provider for creating
MediaWiki.Logger.LegacyLogger instances
* MediaWiki.Logger.LegacyLogger: PSR-3 logger that mimics the historical output
and configuration of wfDebug, wfErrorLog and other global logging functions.
* MediaWiki.Logger.MonologSpi: MediaWiki.Logger.Spi for creating instances
backed by the monolog logging library
* MediaWiki.Logger.Monolog.LegacyHandler: Monolog handler that replicates the
udp2log and file logging functionality of wfErrorLog()
* MediaWiki.Logger.Monolog.WikiProcessor: Monolog log processer that adds host:
wfHostname() and wiki: wfWikiID() to all records

Globals


* $wgMWLoggerDefaultSpi: Specification for creating the default service provider
interface to use with LoggerFactory

[PSR-3]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
[Monolog]: https://github.com/Seldaek/monolog

---

Skin

Skins
=======

Core Skins

MediaWiki includes four core skins:

* Vector: The default skin. Introduced in the 1.16 release (2010), it has been
set as the default in MediaWiki since the 1.17 release (2011), replacing
Monobook.
* Monobook: Named after the black-and-white photo of a book in the page
background. Introduced in the 2004 release of 1.3, it had been the
default skin since then, before being replaced by Vector.
* Modern: An attractive blue/grey theme with sidebar and top bar. Derived from
Monobook.
* Cologne Blue: A lightweight skin with minimal formatting. The oldest of the
currently bundled skins, largely rewritten in 2012 while keeping its
appearance.

Legacy core skins

Several legacy skins were removed in the 1.22 release, as the burden of
supporting them became too heavy to bear. Those were:

* Standard (a.k.a. Classic): The old default skin written by Lee Crocker during
the phase 3 rewrite, in 2002.
* Nostalgia: A skin which looks like Wikipedia did in its first year (2001).
This skin is now used for the old Wikipedia snapshot at
https://nostalgia.wikipedia.org/
* Chick: A lightweight Monobook skin with no sidebar. The sidebar links were
given at the bottom of the page instead.
* Simple: A lightweight skin with a simple white-background sidebar and no top
bar.
* MySkin: Essentially Monobook without the CSS. The idea was that it could be
customised using user-specific or site-wide CSS (see below).


Custom CSS/JS

It is possible to customise the site CSS and JavaScript without editing any
server-side source files. This is done by editing some pages on the wiki:

* MediaWiki:Common.css for skin-independent CSS
* MediaWiki:Common.js for skin-independent JavaScript
* MediaWiki:Vector.css, MediaWiki:Monobook.css, etc. for skin-dependent CSS
* MediaWiki:Vector.js, MediaWiki:Monobook.js, etc. for skin-dependent
JavaScript

These can also be customised on a per-user basis, by editing
User:<name>/vector.css, User:<name>/vector.js, etc.


Custom skins

Several custom skins are available as of 2019. List of all skins is available at
MediaWiki.org.

Installing a skin requires adding its files in a subdirectory under skins/ and
adding an appropriate wfLoadSkin line to LocalSettings.php, similarly to
how extensions are installed.

You can then make that skin the default by adding:

php
$wgDefaultSkin = '<name>';


Or disable it entirely by removing the wfLoadSkin line. (User settings will
not be lost if it's reenabled later.)

See https://www.mediawiki.org/wiki/Manual:Skinning for more information on
writing new skins.

Legacy custom skins

Until MediaWiki 1.25 it used to be possible to just put a <name>.php file in
MediaWiki's skins/ directory, which would be loaded and expected to contain
the Skin<name> class. This way has always been discouraged because of its
limitations (inability to add localisation messages, ResourceLoader modules,
etc.) and awkwardness in managing such skins. For information on migrating skins
using this old method, see
https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery.

---

Title

The MediaWiki software's "Title" class represents article titles, which are used
for many purposes: as the human-readable text title of the article, in the URL
used to access the article, the wikitext link to the article, the key into the
article database, and so on. The class in instantiated from one of these forms
and can be queried for the others, and for other attributes of the title. This
is intended to be an immutable "value" class, so there are no mutator functions.

To get a new instance, call Title::newFromText(). Once instantiated, the
non-static accessor methods can be used, such as getText(), getDBkey(),
getNamespace(), etc. Note that Title::newFromText() may return false if the text
is illegal according to the rules below.

The prefix rules: a title consists of an optional interwiki prefix (such as "m:"
for meta or "de:" for German), followed by an optional namespace, followed by
the remainder of the title. Both interwiki prefixes and namespace prefixes have
the same rules: they contain only letters, digits, space, and underscore, must
start with a letter, are case insensitive, and spaces and underscores are
interchangeable. Prefixes end with a ":". A prefix is only recognized if it is
one of those specifically allowed by the software. For example, "de:name" is a
link to the article "name" in the German Wikipedia, because "de" is recognized
as one of the allowable interwikis. The title "talk:name" is a link to the
article "name" in the "talk" namespace of the current wiki, because "talk" is a
recognized namespace. Both may be present, and if so, the interwiki must
come first, for example, "m:talk:name". If a title begins with a colon as its
first character, no prefixes are scanned for, and the colon is just removed.
Note that because of these rules, it is possible to have articles with colons in
their names. "E. Coli 0157:H7" is a valid title, as is "2001: A Space Odyssey",
because "E. Coli 0157" and "2001" are not valid interwikis or namespaces.

It is not possible to have an article whose bare name includes a namespace or
interwiki prefix.

An initial colon in a title listed in wiki text may however suppress special
handling for interlanguage links, image links, and category links. It is also
used to indicate the main namespace in template inclusions.

Once prefixes have been stripped, the rest of the title processed this way:

* Spaces and underscores are treated as equivalent and each is converted to the
other in the appropriate context (underscore in URL and database keys, spaces
in plain text).
* Multiple consecutive spaces are converted to a single space.
* Leading or trailing space is removed.
* If $wgCapitalLinks is enabled (the default), the first letter is capitalised,
using the capitalisation function of the content language object.
* The unicode characters LRM (U+200E) and RLM (U+200F) are silently stripped.
* Invalid UTF-8 sequences or instances of the replacement character (U+FFFD) are
considered illegal.
* A percent sign followed by two hexadecimal characters is illegal
* Anything that looks like an XML/HTML character reference is illegal
* Any character not matched by the $wgLegalTitleChars regex is illegal
* Zero-length titles (after whitespace stripping) are illegal

All titles except special pages must be less than 255 bytes when encoded with
UTF-8, because that is the size of the database field. Special page titles may
be up to 512 bytes.

Note that Unicode Normal Form C (NFC) is enforced by MediaWiki's user interface
input functions, and so titles will typically be in this form.

getArticleID() needs some explanation: for "internal" articles, it should return
the "page_id" field if the article exists, else it returns 0. For all external
articles it returns 0. All of the IDs for all instances of Title created during
a request are cached, so they can be looked up quickly while rendering wiki text
with lots of internal links. See LinkCache.md.

---

Contenthandler

ContentHandler

The ContentHandler facility adds support for arbitrary content types on wiki pages, instead of relying on wikitext for everything. It was introduced in MediaWiki 1.21.

Each kind of content ("content model") supported by MediaWiki is identified by unique name. The content model determines how a page's content is rendered, compared, stored, edited, and so on.

Built-in content types are:

* wikitext - wikitext, as usual
* javascript - user provided javascript code
* json - simple implementation for use by extensions, etc.
* css - user provided css code
* text - plain text

In PHP, use the corresponding CONTENT_MODEL_XXX constant.

A page's content model is available using the Title::getContentModel() method. A page's default model is determined by ContentHandler::getDefaultModelFor($title) as follows:

* The global setting $wgNamespaceContentModels specifies a content model for the given namespace.
* The hook ContentHandlerDefaultModelFor may be used to override the page's default model.
* Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript model if they end in .css or .js, respectively. Pages in NS_MEDIAWIKI default to the wikitext model otherwise.
* Otherwise, the wikitext model is used.

Note that there is no guarantee that revisions of a page will all have the same content model. To find the content model of the slot of a revision, use SlotRecord::getModel() -
the content model of the main slot can for now be assumed to be the content model for the overall revision.

Architecture

Two class hierarchies are used to provide the functionality associated with the different content models:

* Content interface (and AbstractContent base class) define functionality that acts on the concrete content of a page, and
* ContentHandler base class provides functionality specific to a content model, but not acting on concrete content.

The most important function of ContentHandler is to act as a factory for the appropriate implementation of Content. These Content objects are to be used by MediaWiki everywhere, instead of passing page content around as text. All manipulation and analysis of page content must be done via the appropriate methods of the Content object.

For each content model, a subclass of ContentHandler has to be registered with $wgContentHandlers. The ContentHandler object for a given content model can be obtained using ContentHandler::getForModelID( $id ). Also Title and WikiPage now have getContentHandler() methods for convenience.

ContentHandler objects are singletons that provide functionality specific to the content type, but not directly acting on the content of some page. ContentHandler::makeEmptyContent() and ContentHandler::unserializeContent() can be used to create a Content object of the appropriate type. However, it is recommended to instead use WikiPage::getContent() resp. RevisionRecord::getContent() to get a page's content as a Content object. These two methods should be the ONLY way in which page content is accessed.
For WikiPage::getContent() the content of the main slot is returned, other slots can be retrieved by using RevisionRecord::getContent() and specifying the slot.

Another important function of ContentHandler objects is to define custom action handlers for a content model, see ContentHandler::getActionOverrides(). This is similar to what WikiPage::getActionOverrides() was already doing.

Serialization

With the ContentHandler facility, page content no longer has to be text based. Objects implementing the Content interface are used to represent and handle the content internally. For storage and data exchange, each content model supports at least one serialization format via ContentHandler::serializeContent( $content ). The list of supported formats for a given content model can be accessed using ContentHandler::getSupportedFormats().

Content serialization formats are identified using MIME type like strings. The following formats are built in:

* text/x-wiki - wikitext
* text/javascript - for js pages
* text/css - for css pages
* text/plain - for future use, e.g. with plain text messages.
* text/html - for future use, e.g. with plain html messages.
* application/vnd.php.serialized - for future use with the api and for extensions
* application/json - for future use with the api, and for use by extensions
* application/xml - for future use with the api, and for use by extensions

In PHP, use the corresponding CONTENT_FORMAT_XXX constant.

Note that when using the API to access page content, especially action=edit, action=parse and action=query&prop=revisions, the model and format of the content should always be handled explicitly. Without that information, interpretation of the provided content is not reliable. The same applies to XML dumps generated via maintenance/dumpBackup.php or Special:Export.

Also note that the API will provide encapsulated, serialized content - so if the API was called with format=json, and contentformat is also json (or rather, application/json), the page content is represented as a string containing an escaped json structure. Extensions that use JSON to serialize some types of page content may provide specialized API modules that allow access to that content in a more natural form.

Compatibility

The ContentHandler facility is introduced in a way that should allow all existing code to keep functioning at least for pages that contain wikitext or other text based content. However, a number of functions and hooks have been deprecated in favor of new versions that are aware of the page's content model, and will now generate warnings when used.

Most importantly, the following functions have been deprecated:

* Revision::getText() was deprecated in favor of Revision::getContent() (though the Revision class was later fully removed as part of the migration to Multi-Content Revisions (MCR), see [documentation of mediawiki.org][mediawiki.org/wiki/Multi-Content_Revisions].
* WikiPage::getText() is deprecated in favor of WikiPage::getContent()

Also, the old Article::getContent() (which returns text) is superceded by Article::getContentObject(). However, both methods should be avoided since they do not provide clean access to the page's actual content. For instance, they may return a system message for non-existing pages. Use WikiPage::getContent() instead.

Code that relies on a textual representation of the page content should eventually be rewritten. However, ContentHandler::getContentText() provides a stop-gap that can be used to get text for a page. Its behavior is controlled by $wgContentHandlerTextFallback; per default it will return the text for text based content, and null for any other content.

For rendering page content, Content::getParserOutput() should be used instead of accessing the parser directly. WikiPage::makeParserOptions() can be used to construct appropriate options.

Besides some functions, some hooks have also been replaced by new versions (see hooks.txt for details). These hooks will now trigger a warning when used:

* ArticleAfterFetchContent was replaced by ArticleAfterFetchContentObject, later replaced by ArticleRevisionViewCustom
* ArticleInsertComplete was replaced by PageContentInsertComplete, later replaced by PageSaveComplete
* ArticleSave was replaced by PageContentSave
* ArticleSaveComplete was replaced by PageContentSaveComplete, later replaced by PageSaveComplete
* ArticleViewCustom was replaced by ArticleContentViewCustom, which was later removed entirely
* EditFilterMerged was replaced by EditFilterMergedContent
* EditPageGetDiffText was replaced by EditPageGetDiffContent
* EditPageGetPreviewText was replaced by EditPageGetPreviewContent
* ShowRawCssJs was deprecated in favor of custom rendering implemented in the respective ContentHandler object.

Database Storage

Page content is stored in the database using the same mechanism as before. Non-text content is serialized first.

Each revision's content model and serialization format is stored in the revision table (resp. in the archive table, if the revision was deleted). The page's (current) content model (that is, the content model of the latest revision) is also stored in the page table.

Note however that the content model and format is only stored if it differs from the page's default, as determined by ContentHandler::getDefaultModelFor( $title ). The default values are represented as NULL in the database, to preserve space.

Globals

There are some new globals that can be used to control the behavior of the ContentHandler facility:

* $wgContentHandlers associates content model IDs with the names of the appropriate ContentHandler subclasses or callbacks that create an instance of the appropriate ContentHandler subclass.

* $wgNamespaceContentModels maps namespace IDs to a content model that should be the default for that namespace.

* $wgContentHandlerTextFallback determines how the compatibility method ContentHandler::getContentText() will behave for non-text content:
* 'ignore' causes null to be returned for non-text content (default).
* 'serialize' causes the serialized form of any non-text content to be returned (scary).
* 'fail' causes an exception to be thrown for non-text content (strict).

Caveats

There are some changes in behavior that might be surprising to users:

* Javascript and CSS pages are no longer parsed as wikitext (though pre-save transform is still applied). Most importantly, this means that links, including categorization links, contained in the code will not work.

* action=edit will fail for pages with non-text content, unless the respective ContentHandler implementation has provided a specialized handler for the edit action. This is true for the API as well.

* action=raw will fail for all non-text content. This seems better than serving content in other formats to an unsuspecting recipient. This will also cause client-side diffs to fail.

* File pages provide their own action overrides that do not combine gracefully with any custom handlers defined by a ContentHandler. If for example a File page used a content model with a custom revert action, this would be overridden by WikiFilePage's handler for the revert action.

---

Database

Database Access

Some information about database access in MediaWiki. By Tim Starling, January 2006.

Database Layout

For information about the MediaWiki database layout, such as a description of the tables and their contents, please see:

* The manual
* https://phabricator.wikimedia.org/diffusion/MW/browse/master/maintenance/tables.sql


API

To make a read query, something like this usually suffices:

php
$dbr = wfGetDB( DB_REPLICA );
$res = $dbr->select( / ...see docs... / );
foreach ( $res as $row ) {
...
}

For a write query, use something like:

php
$dbw = wfGetDB( DB_PRIMARY );
$dbw->insert( / ...see docs... / );

We use the convention $dbr for read and $dbw for write to help you keep track of whether the database object is a replica (read-only) or a primary (read/write). If you write to a replica, the world will explode. Or to be precise, a subsequent write query which succeeded on the primary may fail when propagated to the replica due to a unique key collision. Replication will then stop and it may take hours to repair the database and get it back online. Setting read_only in my.cnf on the replica will avoid this scenario, but given the dire consequences, we prefer to have as many checks as possible.

We provide a query() function for raw SQL, but the wrapper functions like select() and insert() are usually more convenient. They take care of things like table prefixes and escaping for you. If you really need to make your own SQL, please read the documentation for tableName() and addQuotes(). You will need both of them.


Basic query optimisation

MediaWiki developers who need to write DB queries should have some understanding of databases and the performance issues associated with them. Patches containing unacceptably slow features will not be accepted. Unindexed queries are generally not welcome in MediaWiki, except in special pages derived from QueryPage. It's a common pitfall for new developers to submit code containing SQL queries which examine huge numbers of rows. Remember that COUNT(*) is O(N), counting rows in a table is like counting beans in a bucket.

Replication

The largest installation of MediaWiki, Wikimedia, uses a large set of replica MySQL servers replicating writes made to a primary MySQL server. It is important to understand the issues associated with this setup if you want to write code destined for Wikipedia.

It's often the case that the best algorithm to use for a given task depends on whether or not replication is in use. Due to our unabashed Wikipedia-centrism, we often just use the replication-friendly version, but if you like, you can use LoadBalancer::getServerCount() > 1 to check to see if replication is in use.

Lag

Lag primarily occurs when large write queries are sent to the primary. Writes on the primary are executed in parallel, but they are executed in serial when they are replicated to the replicas. The primary writes the query to the binlog when the transaction is committed. The replicas poll the binlog and start executing the query as soon as it appears. They can service reads while they are performing a write query, but will not read anything more from the binlog and thus will perform no more writes. This means that if the write query runs for a long time, the replicas will lag behind the primary for the time it takes for the write query to complete.

Lag can be exacerbated by high read load. MediaWiki's load balancer will stop sending reads to a replica when it is lagged by more than 30 seconds. If the load ratios are set incorrectly, or if there is too much load generally, this may lead to a replica permanently hovering around 30 seconds lag.

If all replicas are lagged by more than 30 seconds, MediaWiki will stop writing to the database. All edits and other write operations will be refused, with an error returned to the user. This gives the replicas a chance to catch up. Before we had this mechanism, the replicas would regularly lag by several minutes, making review of recent edits difficult.

In addition to this, MediaWiki attempts to ensure that the user sees events occurring on the wiki in chronological order. A few seconds of lag can be tolerated, as long as the user sees a consistent picture from subsequent requests. This is done by saving the primary binlog position in the session, and then at the start of each request, waiting for the replica to catch up to that position before doing any reads from it. If this wait times out, reads are allowed anyway, but the request is considered to be in "lagged replica mode". Lagged replica mode can be checked by calling LoadBalancer::getLaggedReplicaMode(). The only practical consequence at present is a warning displayed in the page footer.

Lag avoidance

To avoid excessive lag, queries which write large numbers of rows should be split up, generally to write one row at a time. Multi-row INSERT ... SELECT queries are the worst offenders should be avoided altogether. Instead do the select first and then the insert.

Working with lag

Despite our best efforts, it's not practical to guarantee a low-lag environment. Lag will usually be less than one second, but may occasionally be up to 30 seconds. For scalability, it's very important to keep load on the primary low, so simply sending all your queries to the masprimaryter is not the answer. So when you have a genuine need for up-to-date data, the following approach is advised:

1) Do a quick query to the primary for a sequence number or timestamp
2) Run the full query on the replica and check if it matches the data you got
from the primary
3) If it doesn't, run the full query on the primary

To avoid swamping the primary every time the replicas lag, use of this approach should be kept to a minimum. In most cases you should just read from the replica and let the user deal with the delay.

Lock contention

Due to the high write rate on Wikipedia (and some other wikis), MediaWiki developers need to be very careful to structure their writes to avoid long-lasting locks. By default, MediaWiki opens a transaction at the first query, and commits it before the output is sent. Locks will be held from the time when the query is done until the commit. So you can reduce lock time by doing as much processing as possible before you do your write queries.

Often this approach is not good enough, and it becomes necessary to enclose small groups of queries in their own transaction. Use the following syntax:

php
$dbw = wfGetDB( DB_PRIMARY );
$dbw->begin( __METHOD__ );
/ Do queries /
$dbw->commit( __METHOD__ );

Use of locking reads (e.g. the FOR UPDATE clause) is not advised. They are poorly implemented in InnoDB and will cause regular deadlock errors. It's also surprisingly easy to cripple the wiki with lock contention.

Instead of locking reads, combine your existence checks into your write queries, by using an appropriate condition in the WHERE clause of an UPDATE, or by using unique indexes in combination with INSERT IGNORE. Then use the affected row count to see if the query succeeded.

Query groups

MediaWiki supports database query groups, a way to indicate a preferred group of database hosts to use for a given query. Query groups are only supported for connections to child (non-primary) databases, making them only viable for read operations. It should be noted that using query groups does not _guarantee_ a given group of hosts will be used, but rather that the query prefers such group. Making use of query groups can be benficial in many cases.

One benefit is a reduction of cache misses. Directing reads for a category of queries (e.g. all logging queries) to a given host can result in more deterministic and faster performing queries.

Another benefit is that it allows high-traffic wikis to configure some of their database hosts to handle some types of queries more optimally than others. For example, optimizing with different table indices for faster performance.

Query groups are especially beneficial for queries expected to have a long execution time. Such queries can exhaust a database of its resources (e.g. cache space and I/O time), so targeting a specific group of hosts prevents more urgent queries from suffering a performance decrease.

Additionally, expensive queries can delay database maintenance operations which may increase latency for other queries.
For example, while a database read is executing, if other queries have performed updates to any tables those tables must retain all stale versions of its rows until the read is complete. Now, other potentially unrelated queries must now spend additional time scanning over obsolete rows that are waiting to be purged. Directing these long running queries to dedicated hosts helps prevent other queries in suffering a performance hit.

MediaWiki currently supports the following query groups:

* api
* Only use for queries specific to api.php requests; the method ApiBase::getDB() is provided for this purpose.
* dump
* Only use in MediaWiki dump maintenance scripts. In such scripts, all queries, even fast ones, should use this group.
* vslow
* Only use for queries that are expected to have a long execution time. For example, when calculating per-wiki site statistics.

Use the below example syntax to connect to a database when your query falls into one of the above 3 categories:

php
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
$lb->getConnectionRef( DB_REPLICA, 'vslow' );

Supported DBMSs

MediaWiki is written primarily for use with MySQL. Queries are optimized for it and its schema is considered the canonical version. However, MediaWiki does support the following other DBMSs to varying degrees:

* PostgreSQL
* SQLite

More information can be found about each of these databases (known issues, level of support, extra configuration) in the databases subdirectory in this folder.

Use of GROUP BY

MySQL supports GROUP BY without checking anything in the SELECT clause. Other DBMSs (especially Postgres) are stricter and require that all the non-aggregate items in the SELECT clause appear in the GROUP BY. For this reason, it is highly discouraged to use SELECT * with GROUP BY queries.

---

Magicword

Magic Words
====================================

Magic words are localizable keywords used in wikitext. They are used for many
small fragments of text, including:

* The names of parser functions e.g. {{urlencode:...}}
* The names of variables, e.g. {{CURRENTDAY}}
* Double-underscore behavior switches, e.g. __NOTOC__
* Image link parameter names

Magic words have a synonym list, with the canonical English word always present,
and a case sensitivity flag. The MagicWord class provides facilities for
matching a magic word by converting it to a regex.

A magic word has a unique ID. Often, the ID is the canonical English synonym in
lowercase.

To add a magic word in an extension, add a file to the ExtensionMessagesFiles
attribute in extension.json,
and in that file, set a variable called $magicWords. This array is associative
with the language code in the first dimension key and an ID in the second key. The
third level array is numerically indexed: the element with key 0 contains the case
sensitivity flag, with 0 for case-insensitive and 1 for case-sensitive. The
subsequent elements of the array are the synonyms in the relevant language.

To add a magic word in core, add it to $magicWords in MessagesEn.php, following the
comment there.

For example, to add a new parser function in an extension: create a file called
ExtensionName.i18n.magic.php with the following contents:

php
<?php

$magicWords = [];

$magicWords['en'] = [
// Case insensitive.
'mag_custom' => [ 0, 'custom' ],
];

$magicWords['es'] = [
'mag_custom' => [ 0, 'aduanero' ],
];

Then in extension.json:

json
{
"ExtensionMessagesFiles": {
"ExtensionNameMagic": "ExtensionName.i18n.magic.php"
},
"Hooks": {
"ParserFirstCallInit": "MyExtensionHooks::onParserFirstCallInit"
}
}

It is important that the key "ExtensionNameMagic" is unique. It must not be used
by another extension.

And in the class file:

php
<?php

class MyExtensionHooks {
public static function onParserFirstCallInit( $parser ) {
$parser->setFunctionHook( 'mag_custom', [ self::class, 'expandCustom' ] );
return true;
}

public static function expandCustom( $parser, $var1, $var2 ) {
return "custom: var1 is $var1, var2 is $var2";
}
}

- Online documentation (contains more informations):
- Magic words: <https://www.mediawiki.org/wiki/Manual:Magic_words>
- Variables: <https://www.mediawiki.org/wiki/Manual:Variable>
- Parser functions: <https://www.mediawiki.org/wiki/Manual:Parser_functions>

---

Memcached

Memcached
====================================

MediaWiki has optional support for memcached, a "high-performance,
distributed memory object caching system". For general information
on it, see: <http://www.danga.com/memcached/>

Memcached is likely more trouble than a small site will need, but
for a larger site with heavy load, like Wikipedia, it should help
lighten the load on the database servers by caching data and objects
in memory.

Installation
--------------------------------

Packages are available for Fedora, Debian, Ubuntu and probably other
Linux distributions. If there's no package available for your
distribution, you can compile it from source.

Compilation
--------------------------------

* PHP must be compiled with --enable-sockets
* libevent: <http://www.monkey.org/~provos/libevent/>
(as of 2003-08-11, 0.7a is current)
* optionally, epoll-rt patch for Linux kernel:
<http://www.xmailserver.org/linux-patches/nio-improve.html>
* memcached: <http://www.danga.com/memcached/download.bml>
(as of this writing, 1.1.9 is current)

Memcached and libevent are under BSD-style licenses.

The server should run on Linux and other Unix-like systems... you
can run multiple servers on one machine or on multiple machines on
a network; storage can be distributed across multiple servers, and
multiple web servers can use the same cache cluster.

W A R N I N G ! ! ! ! !

Memcached has no security or authentication. Please ensure that your
server is appropriately firewalled, and that the port(s) used for
memcached servers are not publicly accessible. Otherwise, anyone on
the internet can put data into and read data from your cache.

An attacker familiar with MediaWiki internals could use this to steal
passwords and email addresses, or to make themselves a sysop and
install malicious javascript on the site. There may be other types
of vulnerability, no audit has been done -- so be safe and keep it
behind a firewall.

W A R N I N G ! ! ! ! !

Setup
--------------------------------

If you installed memcached using a distro, the daemon should be started
automatically using

/etc/init.d/memcached

To start the daemon manually, use something like:

memcached -d -l 127.0.0.1 -p 11211 -m 64

(to run in daemon mode, accessible only via loopback interface,
on port 11211, using up to 64 MiB of memory)

In your LocalSettings.php file, set:

php
$wgMainCacheType = CACHE_MEMCACHED;
$wgMemCachedServers = [ "127.0.0.1:11211" ];

The wiki should then use memcached to cache various data. To use
multiple servers (physically separate boxes or multiple caches
on one machine on a large-memory x86 box), just add more items
to the array. To increase the weight of a server (say, because
it has twice the memory of the others and you want to spread
usage evenly), make its entry a subarray:

php
$wgMemCachedServers = [
"127.0.0.1:11211", # one gig on this box
[ "192.168.0.1:11211", 2 ] # two gigs on the other box
];

PHP client for memcached
--------------------------------

MediaWiki uses a fork of Ryan T. Dean's pure-PHP memcached client.
It also supports the PECL PHP extension for memcached.

MediaWiki uses the ObjectCache class to retrieve instances of
BagOStuff by purpose, controlled by the following variables:
* $wgMainCacheType
* $wgParserCacheType
* $wgMessageCacheType

If you set one of these to CACHE_NONE, MediaWiki still creates a
BagOStuff object, but calls it to it are no-ops. If the cache daemon
can't be contacted, it should also disable itself fairly smoothly.

Keys used
--------------------------------

(incomplete, out of date)

Date Formatter:

key: $wgDBname:dateformatter
ex: wikidb:dateformatter
stores: a single instance of the DateFormatter class
cleared by: nothing
expiry: one hour

Difference Engine:

key: $wgDBname:diff:version:{MW_DIFF_VERSION}:oldid:$old:newid:$new
ex: wikidb:diff:version:1.11a:oldid:1:newid:2
stores: body of a difference
cleared by: nothing
expiry: one week

Interwiki:

key: $wgDBname:interwiki:$prefix
ex: wikidb:interwiki:w
stores: object from the interwiki table of the database
expiry: $wgInterwikiExpiry
cleared by: nothing

Lag time of the databases:

key: $wgDBname:lag_times
ex: wikidb:lag_times
stores: array mapping the database id to its lag time
expiry: 5 secondes
cleared by: nothing

Localisation:

key: $wgDBname:localisation:$lang
ex: wikidb:localisation:de
stores: array of localisation settings
set in: Language::loadLocalisation()
expiry: none
cleared by: Language::loadLocalisation()

Message Cache:

See MessageCache.php.

Newtalk:

key: $wgDBname:newtalk:ip:$ip
ex: wikidb:newtalk:ip:123.45.67.89
stores: integer, 0 or 1
set in: User::loadFromDatabase()
cleared by: User::saveSettings() # ?
expiry: 30 minutes

Parser Cache:

access: ParserCache
backend: $wgParserCacheType
key: $wgDBname:pcache:idhash:$pageid-$renderkey!$hash
$pageid: id of the page
$renderkey: 1 if action=render, 0 otherwise
$hash: hash of user options applied to the page, see ParserOptions::optionsHash()
ex: wikidb:pcache:idhash:1-0!1!0!!en!2
stores: ParserOutput object
modified by: WikiPage::doEditUpdates() or PoolWorkArticleView::doWork()
expiry: $wgParserCacheExpireTime or less if it contains short lived functions

key: $wgDBname:pcache:idoptions:$pageid
stores: CacheTime object with an additional list of used options for the hash,
serves as ParserCache pointer.
modified by: ParserCache::save()
expiry: The same as the ParserCache entry it points to.

Ping limiter:

controlled by: $wgRateLimits
key: $wgDBname:limiter:action:$action:ip:$ip,
$wgDBname:limiter:action:$action:user:$id,
mediawiki:limiter:action:$action:ip:$ip and
mediawiki:limiter:action:$action:subnet:$sub
ex: wikidb:limiter:action:edit:ip:123.45.67.89,
wikidb:limiter:action:edit:user:1012
mediawiki:limiter:action:edit:ip:123.45.67.89 and
mediawiki:limiter:action:$action:subnet:123.45.67
stores: number of action made by user/ip/subnet
cleared by: nothing
expiry: expiry set for the action and group in $wgRateLimits


Proxy Check: (deprecated)

key: $wgDBname:proxy:ip:$ip
ex: wikidb:proxy:ip:123.45.67.89
stores: 1 if the ip is a proxy
cleared by: nothing
expiry: $wgProxyMemcExpiry

Revision text:

key: $wgDBname:revisiontext:textid:$id
ex: wikidb:revisiontext:textid:1012
stores: text of a revision
cleared by: nothing
expiry: $wgRevisionCacheExpiry

Sessions:

controlled by: $wgSessionsInObjectCache
key: $wgBDname:session:$id
ex: wikidb:session:38d7c5b8d3bfc51egf40c69bc40f8be3
stores: $SESSION, useful when using a multi-sever wiki
expiry: one hour
cleared by: session_destroy()

Sidebar:

access: WANObjectCache
controlled by: $wgEnableSidebarCache
key: $wgDBname:sidebar
ex: wikidb:sidebar
stores: the html output of the sidebar
expiry: $wgSidebarCacheExpiry
cleared by: MessageCache::replace()

Special:Allpages:

key: $wgDBname:allpages:ns:$ns
ex: wikidb:allpages:ns:0
stores: array of pages in a namespace
expiry: one hour
cleared by: nothing

Special:Recentchanges (feed):

backend: $wgMessageCacheType
key: $wgDBname:rcfeed:$format:$limit:$hideminor:$target and
rcfeed:$format:timestamp
ex: wikidb:rcfeed:rss:50:: and rcfeed:rss:timestamp
stores: xml output of feed
expiry: one day
clear by: maintenance/rebuildrecentchanges.php script, or
calling Special:Recentchanges?action=purge&feed=rss,
Special:Recentchanges?action=purge&feed=atom,
but note need $wgGroupPermissions[...]['purge'] permission.

... more to come ...

---

Pageupdater

PageUpdater
===========

This document provides an overview of the usage of PageUpdater and DerivedPageDataUpdater.

PageUpdater


PageUpdater is the canonical way to create page revisions, that is, to perform edits.

PageUpdater is a stateful, handle-like object that allows new revisions to be created on a given wiki page using the saveRevision() method. PageUpdater provides setters for defining the new revision's content as well as meta-data such as change tags. saveRevision() stores the new revision's primary content and metadata, and triggers the necessary updates to derived secondary data and cached artifacts e.g. in the ParserCache and the CDN layer, using a DerivedPageDataUpdater.

PageUpdater instances follow the below life cycle, defined by a number of methods:

+----------------------------+
| |
| new |
| |
+------|--------------|------+
| |
grabParentRevision()-| |
or hasEditConflict()-| |
| |
+--------v-------+ |
| | |
| parent known | |
| | |
Enables---------------+--------|-------+ |
safe operations based on | |-saveRevision()
the parent revision, e.g. | |
section replacement or | |
edit conflict resolution. | |
| |
saveRevision()-| |
| |
+------v--------------v------+
| |
| creation committed |
| |
Enables-----------------+----------------------------+
wasSuccess()
isUnchanged()
isNew()
getState()
getNewRevision()
etc.

The stateful nature of PageUpdater allows it to be used to safely perform transformations that depend on the new revision's parent revision, such as replacing sections or applying 3-way conflict resolution, while protecting against race conditions using a compare-and-swap (CAS) mechanism: after calling code used the grabParentRevision() method to access the edit's logical parent, PageUpdater remembers that revision, and ensure that that revision is still the page's current revision when performing the atomic database update for the revision's primary meta-data when saveRevision() is called. If another revision was created concurrently, saveRevision() will fail, indicating the problem with the "edit-conflict" code in the status object.

Typical usage for programmatic revision creation (with $page being a WikiPage as of 1.32, to be replaced by a repository service later):

php
$updater = $page->newPageUpdater( $user );
$updater->setContent( SlotRecord::MAIN, $content );
$updater->setRcPatrolStatus( RecentChange::PRC_PATROLLED );
$newRev = $updater->saveRevision( $comment );

Usage with content depending on the parent revision

php
$updater = $page->newPageUpdater( $user );
$parent = $updater->grabParentRevision();
$content = $parent->getContent( SlotRecord::MAIN )->replaceSection( $section, $sectionContent );
$updater->setContent( SlotRecord::MAIN, $content );
$newRev = $updater->saveRevision( $comment, EDIT_UPDATE );

In both cases, all secondary updates will be triggered automatically.

DerivedPageDataUpdater


DerivedPageDataUpdater is a stateful, handle-like object that caches derived data representing a revision, and can trigger updates of cached copies of that data, e.g. in the links tables, page_props, the ParserCache, and the CDN layer.

DerivedPageDataUpdater is used by PageUpdater when creating new revisions, but can also be used independently when performing meta data updates during undeletion, import, or when puring a page. It's a stepping stone on the way to a more complete refactoring of WikiPage.

NOTE: Avoid direct usage of DerivedPageDataUpdater. In the future, we want to define interfaces for the different use cases of DerivedPageDataUpdater, particularly providing access to post-PST content and ParserOutput to callbacks during revision creation, which currently use WikiPage::prepareContentForEdit, and allowing updates to be triggered on purge, import, and undeletion, which currently use WikiPage::doEditUpdates() and Content::getSecondaryDataUpdates().

The primary reason for DerivedPageDataUpdater to be stateful is internal caching of state that avoids the re-generation of ParserOutput and re-application of pre-save-transformations (PST).

DerivedPageDataUpdater instances follow the below life cycle, defined by a number of methods:

+---------------------------------------------------------------------+
| |
| new |
| |
+---------------|------------------|------------------|---------------+
| | |
grabCurrentRevision()-| | |
| | |
+-----------v----------+ | |
| | |-prepareContent() |
| knows current | | |
| | | |
Enables------------------+-----|-----|----------+ | |
pageExisted() | | | |
wasRedirect() | |-prepareContent() | |-prepareUpdate()
| | | |
| | +-------------v------------+ |
| | | | |
| +----> has content | |
| | | |
Enables------------------------|----------+--------------------------+ |
isChange() | | |
isCreation() |-prepareUpdate() | |
getSlots() | prepareUpdate()-| |
getTouchedSlotRoles() | | |
getCanonicalParserOutput() | +-----------v------------v-----------------+
| | |
+------------------> has revision |
| |
Enables-------------------------------------------+------------------------|-----------------+
updateParserCache() |
runSecondaryDataUpdates() |-doUpdates()
|
+-----------v---------+
| |
| updates done |
| |
+---------------------+


- grabCurrentRevision() returns the logical parent revision of the target revision. It is guaranteed to always return the same revision for a given DerivedPageDataUpdater instance. If called before prepareUpdate(), this fixates the logical parent to be the page's current revision. If called for the first time after prepareUpdate(), it returns the revision passed as the 'oldrevision' option to prepareUpdate(), or, if that wasn't given, the parent of $revision parameter passed to prepareUpdate().

- prepareContent() is called before the new revision is created, to apply pre-save-transformation (PST) and allow subsequent access to the canonical ParserOutput of the revision. getSlots() and getCanonicalParserOutput() as well as getSecondaryDataUpdates() may be used after prepareContent() was called. Calling prepareContent() with the same parameters again has no effect. Calling it again with mismatching parameters, or calling it after prepareUpdate() was called, triggers a LogicException.

- prepareUpdate() is called after the new revision has been created. This may happen right after the revision was created, on the same instance on which prepareContent() was called, or later (possibly much later), on a fresh instance in a different process, due to deferred or asynchronous updates, or during import, undeletion, purging, etc. prepareUpdate() is required before a call to doUpdates(), and it also enables calls to getSlots() and getCanonicalParserOutput() as well as getSecondaryDataUpdates(). Calling prepareUpdate() with the same parameters again has no effect. Calling it again with mismatching parameters, or calling it with parameters mismatching the ones prepareContent() was called with, triggers a LogicException.

- getSecondaryDataUpdates() returns DataUpdates that represent derived data for the revision. These may be used to update such data, e.g. in ApiPurge, RefreshLinksJob, and the refreshLinks script.

- doUpdates() triggers the updates defined by getSecondaryDataUpdates(), and also causes updates to cached artifacts in the ParserCache, the CDN layer, etc. This is primarily used by PageUpdater, but also by PageArchive during undeletion, and when importing revisions from XML. doUpdates() can only be called after prepareUpdate() was used to initialize the DerivedPageDataUpdater instance for a specific revision. Calling it before prepareUpdate() is called raises a LogicException.

A DerivedPageDataUpdater instance is intended to be re-used during different stages of complex update operations that often involve callbacks to extension code via
MediaWiki's hook mechanism, or deferred or even asynchronous execution of Jobs and DeferredUpdates. Since these mechanisms typically do not provide a way to pass a
DerivedPageDataUpdater directly, WikiPage::getDerivedPageDataUpdater() has to be used to obtain a DerivedPageDataUpdater for the update currently in progress - re-using the same DerivedPageDataUpdater if possible avoids re-generation of ParserOutput objects
and other expensively derived artifacts.

This mechanism for re-using a DerivedPageDataUpdater instance without passing it directly requires a way to ensure that a given DerivedPageDataUpdater instance can actually be used in the calling code's context. For this purpose, WikiPage::getDerivedPageDataUpdater() calls the isReusableFor() method on DerivedPageDataUpdater, which ensures that the given instance is applicable to the given parameters. In other words, isReusableFor() predicts whether calling prepareContent() or prepareUpdate() with a given set of parameters will trigger a LogicException. In that case, WikiPage::getDerivedPageDataUpdater() creates a fresh DerivedPageDataUpdater instance.

---

Schema

Schema
======

The most up-to-date schema for the tables in the database
will always be tables.sql in the maintenance directory,
which is called from the installation script.

That file has been commented with details of the usage for
each table and field.

Historical information and some other notes are available at
<https://www.mediawiki.org/wiki/Manual:Database_layout>.

---

Sitelist

Sitelist
========

This document describes the XML format used to represent information about external sites known to a MediaWiki installation. This information about external sites is used to allow "inter-wiki" links, cross-language navigation, as well as close integration via direct access to the other site's web API or even directly to their database.

Lists of external sites can be imported and exported using the importSites.php and exportSites.php scripts. In the database, external sites are described by the sites and site_ids tables.

The formal specification of the format used by importSites.php and exportSites.php can be found in the sitelist-1.0.xsd file. Below is an example and a brief description of what the individual XML elements and attributes mean:

xml
<sites version="1.0">
<site>
<globalid>acme.com</globalid>
<localid type="interwiki">acme</localid>
<group>Vendor</group>
<path type="link">http://acme.com/</path>
<source>meta.wikimedia.org</source>
</site>
<site type="mediawiki">
<globalid>de.wikidik.example</globalid>
<localid type="equivalent">de</localid>
<group>Dictionary</group>
<forward/>
<path type="page_path">http://acme.com/</path>
</site>
</sites>


The XML elements are used as follows:

- sites: The root element, containing a set of site tags. May have a version attribute with the value 1.0.
- site: A site entry, representing an external website. May have a type attribute with one of the following values:
+ unknown: (default) any website
+ mediawiki: A MediaWiki site
- globalid: A unique identifier for the site. For a given site, the same unique global ID must be used across all wikis in a wiki farm (aka wiki family).
- localid: An identifier for the site, for use on the local wiki. Multiple local IDs may be assigned to a given site. The same local ID can be used to refer to different sites by different wikis on the same farm/family. The localid element may have a type attribute with one of the following values:
+ interwiki: Used as an "interwiki" link prefix, for creating cross-wiki links.
+ equivalent: Used as a "language" link prefix, for cross-linking equivalent content in different languages.
- group: The site group (e.g. wiki family) the site belongs to.
- path: A URL template for accessing resources on the site. Several paths may be defined for a given site, for accessing different kinds of resources, identified by the type attribute, using one of the following values:
+ link: Generic URL template, often the document root.
+ page_path: (for mediawiki sites) URL template for wiki pages (corresponds to the target wiki's $wgArticlePath setting)
+ file_path: (for mediawiki sites) URL pattern for application entry points and resources (corresponds to the target wiki's $wgScriptPath setting).
- forward: Whether using a prefix defined by a localid tag in the URL will cause the request to be redirected to the corresponding page on the target wiki (currently unused). E.g. whether <http://wiki.acme.com/wiki/foo:Buzz> should be forwarded to <http://wiki.foo.com/read/Buzz>. (CAVEAT: not yet implement, can be specified but has no effect)

---