docs: Move, clarify and cross-ref "DI Principles" for easy reference

Loosely based on https://phabricator.wikimedia.org/T218555,
https://phabricator.wikimedia.org/T242835#6064656, and
https://phabricator.wikimedia.org/T302627#7846955.

Change-Id: Ib649297ba28d75eca3c96235124a97ea38dde72c
This commit is contained in:
Timo Tijhof 2022-04-12 02:25:37 +01:00 committed by Krinkle
parent 83a0742de6
commit e94e3e9545
4 changed files with 142 additions and 98 deletions

View file

@ -1,65 +1,123 @@
Dependency Injection
Dependency Injection {#dependencyinjection}
=======
This is an overview of how MediaWiki makes use of dependency injection.
The design described here grew from the discussion of RFC
[T384](https://phabricator.wikimedia.org/T384).
This is an overview of how MediaWiki uses of dependency injection.
The design originates from [RFC T384](https://phabricator.wikimedia.org/T384).
The term "dependency injection" (DI) refers to a pattern on object oriented
programming that tries to improve modularity by reducing strong coupling
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
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 that
based on two main types 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 the value objects.
As of the beginning of 2016 (MW version 1.27), MediaWiki is only starting to
use the DI approach. Much of the code still relies on global state or direct
instantiation, resulting in a highly cyclical dependency graph.
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 for services in
MediaWiki. `MediaWikiServices::getInstance()` returns the default service
locator instance, which can be used to gain access to default instances of
various services. MediaWikiServices however also allows new services to be
defined and default services to be redefined. Services are defined or
redefined by providing a callback function, the "instantiator" function,
that will return a new instance of the service.
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
in the files listed by `$wgServiceWiringFiles`, thereby "bootstrapping" the
DI framework. Per default, `$wgServiceWiringFiles` lists
includes/ServiceWiring.php, which defines all default service
implementations, and specifies how they depend on each other ("wiring").
When a new service is added to MediaWiki core, an instantiator function
that will create the appropriate default instance for that service must
be added to ServiceWiring.php. This makes the service available through
the generic getService() method on the service locator returned by
`MediaWikiServices::getInstance()`.
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 define or redefined services by calling methods on the default
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
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 when services
are created. We use the term "service locator" for the top level factory
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](https://phabricator.wikimedia.org/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
@ -81,7 +139,6 @@ 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
@ -96,13 +153,11 @@ 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

View file

@ -1,4 +1,22 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace MediaWiki;
@ -178,39 +196,13 @@ use Wikimedia\UUID\GlobalIdGenerator;
/**
* Service locator for MediaWiki core services.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* Refer to includes/ServiceWiring.php for the default implementations.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @see [Dependency Injection](@ref dependencyinjection) in docs/Injection.md
* for the principles of DI and how to use it MediaWiki core.
*
* @since 1.27
*/
/**
* MediaWikiServices is the service locator for the application scope of MediaWiki.
* Its implemented as a simple configurable DI container.
* MediaWikiServices acts as a top level factory/registry for top level services, and builds
* the network of service objects that defines MediaWiki's application logic.
* It acts as an entry point to MediaWiki's dependency injection mechanism.
*
* Services are defined in the "wiring" array passed to the constructor,
* or by calling defineService().
*
* @see docs/Injection.md for an overview of using dependency injection in the
* MediaWiki code base.
*/
class MediaWikiServices extends ServiceContainer {
use NonSerializableTrait;

View file

@ -1,6 +1,27 @@
<?php
/**
* Default wiring for MediaWiki services.
* Service implemenations for %MediaWiki core.
*
* This file returns the array loaded by the MediaWikiServices class
* for use through `MediaWiki\MediaWikiServices::getInstance()`
*
* @see [Dependency Injection](@ref dependencyinjection) in docs/Injection.md
* for the principles of DI and how to use it MediaWiki core.
*
* Reminder:
*
* - ServiceWiring is NOT a cache for arbitrary singletons.
*
* - Services MUST NOT vary their behaviour on global state, especially not
* WebRequest, RequestContext (T218555), or other details of the current
* request or CLI process (e.g. "current" user or title). Doing so may
* cause a chain reaction and cause serious data corruption.
*
* Refer to [DI Principles](@ref di-principles) in docs/Injection.md for
* how and why we avoid this, as well as for limited exemptions to these
* principles.
*
* -------
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -18,30 +39,6 @@
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*
* This file is loaded by MediaWiki\MediaWikiServices::getInstance() during the
* bootstrapping of the dependency injection framework.
*
* This file returns an array that associates service name with instantiator functions
* that create the default instances for the services used by MediaWiki core.
* For every service that MediaWiki core requires, an instantiator must be defined in
* this file.
*
* Note that, ideally, all information used to instantiate service objects should come
* from configuration. Information derived from the current request is acceptable, but
* only where there is no feasible alternative. It is preferred that such information
* (like the client IP, the acting user's identity, requested title, etc) be passed to
* the service object's methods as parameters. This makes the flow of information more
* obvious, and makes it easier to understand the behavior of services.
*
* @note As of version 1.27, MediaWiki is only beginning to use dependency injection.
* The services defined here do not yet fully represent all services used by core,
* much of the code still relies on global state for this accessing services.
*
* @since 1.27
*
* @see docs/Injection.md for an overview of using dependency injection in the
* MediaWiki code base.
*/
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;

View file

@ -16,14 +16,14 @@ file system to distributed object stores). The types include:
* FileBackendMultiWrite (useful for transitioning from one backend to another)
Configuration documentation for each type of backend is to be found in their
__construct() inline documentation.
`__construct()` inline documentation.
Setup
-----
File backends are registered in LocalSettings.php via the global variable
$wgFileBackends. To access one of those defined backends, one would use
FileBackendStore::get( <name> ) which will bring back a FileBackend object
`FileBackendStore::get( <name> )` which will bring back a FileBackend object
handle. Such handles are reused for any subsequent get() call (via singleton).
The FileBackends objects are caching request calls such as file stats,
SHA1 requests or TCP connection handles.