Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.79% |
91 / 95 |
|
75.00% |
6 / 8 |
CRAP | |
0.00% |
0 / 1 |
PageProps | |
96.81% |
91 / 94 |
|
75.00% |
6 / 8 |
37 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
ensureCacheSize | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getProperties | |
96.88% |
31 / 32 |
|
0.00% |
0 / 1 |
9 | |||
getAllProperties | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
8 | |||
getGoodIDs | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
10 | |||
getCachedProperty | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
getCachedProperties | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
cacheProperties | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Page; |
22 | |
23 | use MapCacheLRU; |
24 | use MediaWiki\Cache\LinkBatchFactory; |
25 | use MediaWiki\Title\Title; |
26 | use MediaWiki\Title\TitleArrayFromResult; |
27 | use Wikimedia\Rdbms\IConnectionProvider; |
28 | |
29 | /** |
30 | * Gives access to properties of a page. |
31 | * |
32 | * @since 1.27 |
33 | * @ingroup Page |
34 | */ |
35 | class PageProps { |
36 | /* TTL in seconds */ |
37 | private const CACHE_TTL = 10; |
38 | /* max cached pages */ |
39 | private const CACHE_SIZE = 100; |
40 | |
41 | private LinkBatchFactory $linkBatchFactory; |
42 | private IConnectionProvider $dbProvider; |
43 | private MapCacheLRU $cache; |
44 | |
45 | /** |
46 | * @param LinkBatchFactory $linkBatchFactory |
47 | * @param IConnectionProvider $dbProvider |
48 | */ |
49 | public function __construct( |
50 | LinkBatchFactory $linkBatchFactory, |
51 | IConnectionProvider $dbProvider |
52 | ) { |
53 | $this->linkBatchFactory = $linkBatchFactory; |
54 | $this->dbProvider = $dbProvider; |
55 | $this->cache = new MapCacheLRU( self::CACHE_SIZE ); |
56 | } |
57 | |
58 | /** |
59 | * Ensure that cache has at least this size |
60 | * @param int $size |
61 | */ |
62 | public function ensureCacheSize( $size ) { |
63 | if ( $this->cache->getMaxSize() < $size ) { |
64 | $this->cache->setMaxSize( $size ); |
65 | } |
66 | } |
67 | |
68 | /** |
69 | * Fetch one or more properties for one or more Titles. |
70 | * |
71 | * Returns an associative array mapping page ID to property value. |
72 | * |
73 | * If a single Title is provided without an array, the output will still |
74 | * be returned as an array by page ID. |
75 | * |
76 | * Pages in the provided set of Titles that do not have a value for |
77 | * any of the properties will not appear in the returned array. |
78 | * |
79 | * If a single property name is requested, it does not need to be passed |
80 | * in as an array. In that case, the return array will map directly from |
81 | * page ID to property value. Otherwise, a multi-dimensional array is |
82 | * returned keyed by page ID, then property name, to property value. |
83 | * |
84 | * An empty array will be returned if no matching properties were found. |
85 | * |
86 | * @param iterable<PageIdentity>|PageIdentity $titles |
87 | * @param string[]|string $propertyNames |
88 | * @return array<int,string|array<string,string>> Keyed by page ID and property name |
89 | * to property value |
90 | */ |
91 | public function getProperties( $titles, $propertyNames ) { |
92 | if ( is_array( $propertyNames ) ) { |
93 | $gotArray = true; |
94 | } else { |
95 | $propertyNames = [ $propertyNames ]; |
96 | $gotArray = false; |
97 | } |
98 | |
99 | $values = []; |
100 | $goodIDs = $this->getGoodIDs( $titles ); |
101 | $queryIDs = []; |
102 | foreach ( $goodIDs as $pageID ) { |
103 | foreach ( $propertyNames as $propertyName ) { |
104 | $propertyValue = $this->getCachedProperty( $pageID, $propertyName ); |
105 | if ( $propertyValue === false ) { |
106 | $queryIDs[] = $pageID; |
107 | break; |
108 | } elseif ( $gotArray ) { |
109 | $values[$pageID][$propertyName] = $propertyValue; |
110 | } else { |
111 | $values[$pageID] = $propertyValue; |
112 | } |
113 | } |
114 | } |
115 | |
116 | if ( $queryIDs ) { |
117 | $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder(); |
118 | $queryBuilder->select( [ 'pp_page', 'pp_propname', 'pp_value' ] ) |
119 | ->from( 'page_props' ) |
120 | ->where( [ 'pp_page' => $queryIDs, 'pp_propname' => $propertyNames ] ) |
121 | ->caller( __METHOD__ ); |
122 | $result = $queryBuilder->fetchResultSet(); |
123 | |
124 | foreach ( $result as $row ) { |
125 | $pageID = $row->pp_page; |
126 | $propertyName = $row->pp_propname; |
127 | $propertyValue = $row->pp_value; |
128 | $this->cache->setField( $pageID, $propertyName, $propertyValue ); |
129 | if ( $gotArray ) { |
130 | $values[$pageID][$propertyName] = $propertyValue; |
131 | } else { |
132 | $values[$pageID] = $propertyValue; |
133 | } |
134 | } |
135 | } |
136 | |
137 | return $values; |
138 | } |
139 | |
140 | /** |
141 | * Get all page properties of one or more page titles. |
142 | * |
143 | * Given one or more Titles, returns an array keyed by page ID to another |
144 | * array from property names to property values. |
145 | * |
146 | * If a single Title is provided without an array, the output will still |
147 | * be returned as an array by page ID. |
148 | * |
149 | * Pages in the provided set of Titles that do have no page properties, |
150 | * will not get a page ID key in the returned array. |
151 | * |
152 | * An empty array will be returned if none of the titles have any page properties. |
153 | * |
154 | * @param iterable<PageIdentity>|PageIdentity $titles |
155 | * @return array<int,array<string,string>> Keyed by page ID and property name to property value |
156 | */ |
157 | public function getAllProperties( $titles ) { |
158 | $values = []; |
159 | $goodIDs = $this->getGoodIDs( $titles ); |
160 | $queryIDs = []; |
161 | foreach ( $goodIDs as $pageID ) { |
162 | $pageProperties = $this->getCachedProperties( $pageID ); |
163 | if ( $pageProperties === false ) { |
164 | $queryIDs[] = $pageID; |
165 | } else { |
166 | $values[$pageID] = $pageProperties; |
167 | } |
168 | } |
169 | |
170 | if ( $queryIDs != [] ) { |
171 | $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder(); |
172 | $queryBuilder->select( [ 'pp_page', 'pp_propname', 'pp_value' ] ) |
173 | ->from( 'page_props' ) |
174 | ->where( [ 'pp_page' => $queryIDs ] ) |
175 | ->caller( __METHOD__ ); |
176 | $result = $queryBuilder->fetchResultSet(); |
177 | |
178 | $currentPageID = 0; |
179 | $pageProperties = []; |
180 | foreach ( $result as $row ) { |
181 | $pageID = $row->pp_page; |
182 | if ( $currentPageID != $pageID ) { |
183 | if ( $pageProperties ) { |
184 | // @phan-suppress-next-line PhanTypeMismatchArgument False positive |
185 | $this->cacheProperties( $currentPageID, $pageProperties ); |
186 | $values[$currentPageID] = $pageProperties; |
187 | } |
188 | $currentPageID = $pageID; |
189 | $pageProperties = []; |
190 | } |
191 | $pageProperties[$row->pp_propname] = $row->pp_value; |
192 | } |
193 | if ( $pageProperties != [] ) { |
194 | // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable pageID set when used |
195 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable pageID set when used |
196 | $this->cacheProperties( $pageID, $pageProperties ); |
197 | // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable pageID set when used |
198 | // @phan-suppress-next-line PhanTypeMismatchDimAssignment pageID set when used |
199 | $values[$pageID] = $pageProperties; |
200 | } |
201 | } |
202 | |
203 | return $values; |
204 | } |
205 | |
206 | /** |
207 | * @param iterable<PageIdentity>|PageIdentity $titles |
208 | * @return int[] List of good page IDs |
209 | */ |
210 | private function getGoodIDs( $titles ) { |
211 | $result = []; |
212 | if ( is_iterable( $titles ) ) { |
213 | if ( $titles instanceof TitleArrayFromResult || |
214 | ( is_array( $titles ) && reset( $titles ) instanceof Title |
215 | ) ) { |
216 | // If the first element is a Title, assume all elements are Titles, |
217 | // and pre-fetch their IDs using a batch query. For PageIdentityValues |
218 | // or PageStoreRecords, this is not necessary, since they already |
219 | // know their ID. |
220 | $this->linkBatchFactory->newLinkBatch( $titles )->execute(); |
221 | } |
222 | |
223 | foreach ( $titles as $title ) { |
224 | // Until we only allow ProperPageIdentity, Title objects |
225 | // can deceive us with an unexpected Special page |
226 | if ( $title->canExist() ) { |
227 | $pageID = $title->getId(); |
228 | if ( $pageID > 0 ) { |
229 | $result[] = $pageID; |
230 | } |
231 | } |
232 | } |
233 | } else { |
234 | // Until we only allow ProperPageIdentity, Title objects |
235 | // can deceive us with an unexpected Special page |
236 | if ( $titles->canExist() ) { |
237 | $pageID = $titles->getId(); |
238 | if ( $pageID > 0 ) { |
239 | $result[] = $pageID; |
240 | } |
241 | } |
242 | } |
243 | return $result; |
244 | } |
245 | |
246 | /** |
247 | * Get a property from the cache. |
248 | * |
249 | * @param int $pageID page ID of page being queried |
250 | * @param string $propertyName name of property being queried |
251 | * @return string|bool property value array or false if not found |
252 | */ |
253 | private function getCachedProperty( $pageID, $propertyName ) { |
254 | if ( $this->cache->hasField( $pageID, $propertyName, self::CACHE_TTL ) ) { |
255 | return $this->cache->getField( $pageID, $propertyName ); |
256 | } |
257 | if ( $this->cache->hasField( 0, $pageID, self::CACHE_TTL ) ) { |
258 | $pageProperties = $this->cache->getField( 0, $pageID ); |
259 | if ( isset( $pageProperties[$propertyName] ) ) { |
260 | return $pageProperties[$propertyName]; |
261 | } |
262 | } |
263 | return false; |
264 | } |
265 | |
266 | /** |
267 | * Get properties from the cache. |
268 | * |
269 | * @param int $pageID page ID of page being queried |
270 | * @return string|bool property value array or false if not found |
271 | */ |
272 | private function getCachedProperties( $pageID ) { |
273 | if ( $this->cache->hasField( 0, $pageID, self::CACHE_TTL ) ) { |
274 | return $this->cache->getField( 0, $pageID ); |
275 | } |
276 | return false; |
277 | } |
278 | |
279 | /** |
280 | * Save properties to the cache. |
281 | * |
282 | * @param int $pageID page ID of page being cached |
283 | * @param string[] $pageProperties associative array of page properties to be cached |
284 | */ |
285 | private function cacheProperties( $pageID, $pageProperties ) { |
286 | $this->cache->clear( $pageID ); |
287 | $this->cache->setField( 0, $pageID, $pageProperties ); |
288 | } |
289 | } |
290 | |
291 | /** @deprecated class alias since 1.40 */ |
292 | class_alias( PageProps::class, 'PageProps' ); |