Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
82.98% |
39 / 47 |
|
44.44% |
4 / 9 |
CRAP | |
0.00% |
0 / 1 |
ExternalStoreFactory | |
82.98% |
39 / 47 |
|
44.44% |
4 / 9 |
29.33 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getProtocols | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getWriteBaseUrls | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getStore | |
82.61% |
19 / 23 |
|
0.00% |
0 / 1 |
11.64 | |||
getStoreForUrl | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
getStoreLocationFromUrl | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
getUrlsByProtocol | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
splitStorageUrl | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
4.25 |
1 | <?php |
2 | |
3 | use MediaWiki\MediaWikiServices; |
4 | use Psr\Log\LoggerAwareInterface; |
5 | use Psr\Log\LoggerInterface; |
6 | use Psr\Log\NullLogger; |
7 | |
8 | /** |
9 | * @see ExternalStoreAccess |
10 | * @internal Use the ExternalStoreAccess service instead. |
11 | * @since 1.31 |
12 | * @ingroup ExternalStorage |
13 | */ |
14 | class ExternalStoreFactory implements LoggerAwareInterface { |
15 | /** @var string[] List of storage access protocols */ |
16 | private $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 */ |
22 | private $logger; |
23 | /** @var ExternalStoreMedium[] */ |
24 | private $stores = []; |
25 | |
26 | /** |
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 |
31 | */ |
32 | public function __construct( |
33 | array $externalStores, |
34 | array $defaultStores, |
35 | string $localDomainId, |
36 | ?LoggerInterface $logger = null |
37 | ) { |
38 | $this->protocols = array_map( 'strtolower', $externalStores ); |
39 | $this->writeBaseUrls = $defaultStores; |
40 | $this->localDomainId = $localDomainId; |
41 | $this->logger = $logger ?: new NullLogger(); |
42 | } |
43 | |
44 | public function setLogger( LoggerInterface $logger ) { |
45 | $this->logger = $logger; |
46 | } |
47 | |
48 | /** |
49 | * @return string[] List of active store types/protocols (lowercased), e.g. [ "db" ] |
50 | * @since 1.34 |
51 | */ |
52 | public function getProtocols() { |
53 | return $this->protocols; |
54 | } |
55 | |
56 | /** |
57 | * @return string[] List of default base URLs for writes, e.g. [ "DB://cluster1" ] |
58 | * @since 1.34 |
59 | */ |
60 | public function getWriteBaseUrls() { |
61 | return $this->writeBaseUrls; |
62 | } |
63 | |
64 | /** |
65 | * Get an external store object of the given type, with the given parameters |
66 | * |
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). |
70 | * |
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 |
75 | */ |
76 | public function getStore( $proto, array $params = [] ) { |
77 | $cacheKey = $proto . ':' . json_encode( $params ); |
78 | if ( isset( $this->stores[$cacheKey] ) ) { |
79 | return $this->stores[$cacheKey]; |
80 | } |
81 | $protoLowercase = strtolower( $proto ); // normalize |
82 | if ( !$this->protocols || !in_array( $protoLowercase, $this->protocols ) ) { |
83 | throw new ExternalStoreException( "Protocol '$proto' is not enabled." ); |
84 | } |
85 | |
86 | if ( $protoLowercase === 'db' ) { |
87 | $class = 'ExternalStoreDB'; |
88 | } else { |
89 | $class = 'ExternalStore' . ucfirst( $proto ); |
90 | } |
91 | if ( isset( $params['wiki'] ) ) { |
92 | $params += [ 'domain' => $params['wiki'] ]; // b/c |
93 | } |
94 | if ( !isset( $params['domain'] ) || $params['domain'] === false ) { |
95 | $params['domain'] = $this->localDomainId; // default |
96 | $params['isDomainImplicit'] = true; // b/c for ExternalStoreDB |
97 | } |
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(); |
104 | } |
105 | $params['logger'] = $this->logger; |
106 | |
107 | if ( !class_exists( $class ) ) { |
108 | throw new ExternalStoreException( "Class '$class' is not defined." ); |
109 | } |
110 | |
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]; |
114 | } |
115 | |
116 | /** |
117 | * Get the ExternalStoreMedium for a given URL |
118 | * |
119 | * $url is either of the form: |
120 | * - a) "<proto>://<location>/<path>", for retrieval, or |
121 | * - b) "<proto>://<location>", for storage |
122 | * |
123 | * @param string $url |
124 | * @param array $params Map of ExternalStoreMedium::__construct context parameters |
125 | * @return ExternalStoreMedium |
126 | * @throws ExternalStoreException When the protocol is missing or not recognized |
127 | * @since 1.34 |
128 | */ |
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'" ); |
133 | } |
134 | |
135 | return $this->getStore( $proto, $params ); |
136 | } |
137 | |
138 | /** |
139 | * Get the location within the appropriate store for a given a URL |
140 | * |
141 | * @param string $url |
142 | * @return string |
143 | * @throws ExternalStoreException |
144 | * @since 1.34 |
145 | */ |
146 | public function getStoreLocationFromUrl( $url ) { |
147 | [ , $location ] = self::splitStorageUrl( $url ); |
148 | if ( $location == '' ) { // bad URL |
149 | throw new ExternalStoreException( "Invalid URL '$url'" ); |
150 | } |
151 | |
152 | return $location; |
153 | } |
154 | |
155 | /** |
156 | * @param string[] $urls |
157 | * @return string[][] Map of (protocol => list of URLs) |
158 | * @throws ExternalStoreException |
159 | * @since 1.34 |
160 | */ |
161 | public function getUrlsByProtocol( array $urls ) { |
162 | $urlsByProtocol = []; |
163 | foreach ( $urls as $url ) { |
164 | [ $proto, ] = self::splitStorageUrl( $url ); |
165 | $urlsByProtocol[$proto][] = $url; |
166 | } |
167 | |
168 | return $urlsByProtocol; |
169 | } |
170 | |
171 | /** |
172 | * @param string $storeUrl |
173 | * @return string[] (protocol, store location or location-qualified path) |
174 | * @throws ExternalStoreException |
175 | */ |
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'" ); |
180 | } |
181 | |
182 | return $parts; |
183 | } |
184 | } |