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:
parent
83a0742de6
commit
e94e3e9545
4 changed files with 142 additions and 98 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue