3 use MediaWiki\MediaWikiServices
;
4 use Psr\Log\LoggerAwareInterface
;
5 use Psr\Log\LoggerInterface
;
6 use Psr\Log\NullLogger
;
9 * @see ExternalStoreAccess
10 * @internal Use the ExternalStoreAccess service instead.
12 * @ingroup ExternalStorage
14 class ExternalStoreFactory
implements LoggerAwareInterface
{
15 /** @var string[] List of storage access protocols */
17 /** @var string[] List of base storage URLs that define locations for writes */
18 private $writeBaseUrls;
19 /** @var string Default database domain to store content under */
20 private $localDomainId;
21 /** @var LoggerInterface */
23 /** @var ExternalStoreMedium[] */
27 * @param string[] $externalStores See $wgExternalStores
28 * @param string[] $defaultStores See $wgDefaultExternalStore
29 * @param string $localDomainId Local database/wiki ID
30 * @param LoggerInterface|null $logger
32 public function __construct(
33 array $externalStores,
35 string $localDomainId,
36 ?LoggerInterface
$logger = null
38 $this->protocols
= array_map( 'strtolower', $externalStores );
39 $this->writeBaseUrls
= $defaultStores;
40 $this->localDomainId
= $localDomainId;
41 $this->logger
= $logger ?
: new NullLogger();
44 public function setLogger( LoggerInterface
$logger ) {
45 $this->logger
= $logger;
49 * @return string[] List of active store types/protocols (lowercased), e.g. [ "db" ]
52 public function getProtocols() {
53 return $this->protocols
;
57 * @return string[] List of default base URLs for writes, e.g. [ "DB://cluster1" ]
60 public function getWriteBaseUrls() {
61 return $this->writeBaseUrls
;
65 * Get an external store object of the given type, with the given parameters
67 * The 'domain' field in $params will be set to the local DB domain if it is unset
68 * or false. A special 'isDomainImplicit' flag is set when this happens, which should
69 * only be used to handle legacy DB domain configuration concerns (e.g. T200471).
71 * @param string $proto Type of external storage, should be a value in $wgExternalStores
72 * @param array $params Map of ExternalStoreMedium::__construct context parameters.
73 * @return ExternalStoreMedium The store class or false on error
74 * @throws ExternalStoreException When $proto is not recognized
76 public function getStore( $proto, array $params = [] ) {
77 $cacheKey = $proto . ':' . json_encode( $params );
78 if ( isset( $this->stores
[$cacheKey] ) ) {
79 return $this->stores
[$cacheKey];
81 $protoLowercase = strtolower( $proto ); // normalize
82 if ( !$this->protocols ||
!in_array( $protoLowercase, $this->protocols
) ) {
83 throw new ExternalStoreException( "Protocol '$proto' is not enabled." );
86 if ( $protoLowercase === 'db' ) {
87 $class = 'ExternalStoreDB';
89 $class = 'ExternalStore' . ucfirst( $proto );
91 if ( isset( $params['wiki'] ) ) {
92 $params +
= [ 'domain' => $params['wiki'] ]; // b/c
94 if ( !isset( $params['domain'] ) ||
$params['domain'] === false ) {
95 $params['domain'] = $this->localDomainId
; // default
96 $params['isDomainImplicit'] = true; // b/c for ExternalStoreDB
98 // @TODO: ideally, this class should not hardcode what classes need what backend factory
99 // objects. For now, inject the factory instances into __construct() for those that do.
100 if ( $protoLowercase === 'db' ) {
101 $params['lbFactory'] = MediaWikiServices
::getInstance()->getDBLoadBalancerFactory();
102 } elseif ( $protoLowercase === 'mwstore' ) {
103 $params['fbGroup'] = MediaWikiServices
::getInstance()->getFileBackendGroup();
105 $params['logger'] = $this->logger
;
107 if ( !class_exists( $class ) ) {
108 throw new ExternalStoreException( "Class '$class' is not defined." );
111 // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
112 $this->stores
[$cacheKey] = new $class( $params );
113 return $this->stores
[$cacheKey];
117 * Get the ExternalStoreMedium for a given URL
119 * $url is either of the form:
120 * - a) "<proto>://<location>/<path>", for retrieval, or
121 * - b) "<proto>://<location>", for storage
124 * @param array $params Map of ExternalStoreMedium::__construct context parameters
125 * @return ExternalStoreMedium
126 * @throws ExternalStoreException When the protocol is missing or not recognized
129 public function getStoreForUrl( $url, array $params = [] ) {
130 [ $proto, $path ] = self
::splitStorageUrl( $url );
131 if ( $path == '' ) { // bad URL
132 throw new ExternalStoreException( "Invalid URL '$url'" );
135 return $this->getStore( $proto, $params );
139 * Get the location within the appropriate store for a given a URL
143 * @throws ExternalStoreException
146 public function getStoreLocationFromUrl( $url ) {
147 [ , $location ] = self
::splitStorageUrl( $url );
148 if ( $location == '' ) { // bad URL
149 throw new ExternalStoreException( "Invalid URL '$url'" );
156 * @param string[] $urls
157 * @return string[][] Map of (protocol => list of URLs)
158 * @throws ExternalStoreException
161 public function getUrlsByProtocol( array $urls ) {
162 $urlsByProtocol = [];
163 foreach ( $urls as $url ) {
164 [ $proto, ] = self
::splitStorageUrl( $url );
165 $urlsByProtocol[$proto][] = $url;
168 return $urlsByProtocol;
172 * @param string $storeUrl
173 * @return string[] (protocol, store location or location-qualified path)
174 * @throws ExternalStoreException
176 private static function splitStorageUrl( $storeUrl ) {
177 $parts = explode( '://', $storeUrl );
178 if ( count( $parts ) != 2 ||
$parts[0] === '' ||
$parts[1] === '' ) {
179 throw new ExternalStoreException( "Invalid storage URL '$storeUrl'" );