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 | public function __construct( |
46 | LinkBatchFactory $linkBatchFactory, |
47 | IConnectionProvider $dbProvider |
48 | ) { |
49 | $this->linkBatchFactory = $linkBatchFactory; |
50 | $this->dbProvider = $dbProvider; |
51 | $this->cache = new MapCacheLRU( self::CACHE_SIZE ); |
52 | } |
53 | |
54 | /** |
55 | * Ensure that cache has at least this size |
56 | * @param int $size |
57 | */ |
58 | public function ensureCacheSize( $size ) { |
59 | if ( $this->cache->getMaxSize() < $size ) { |
60 | $this->cache->setMaxSize( $size ); |
61 | } |
62 | } |
63 | |
64 | /** |
65 | * Fetch one or more properties for one or more Titles. |
66 | * |
67 | * Returns an associative array mapping page ID to property value. |
68 | * |
69 | * If a single Title is provided without an array, the output will still |
70 | * be returned as an array by page ID. |
71 | * |
72 | * Pages in the provided set of Titles that do not have a value for |
73 | * any of the properties will not appear in the returned array. |
74 | * |
75 | * If a single property name is requested, it does not need to be passed |
76 | * in as an array. In that case, the return array will map directly from |
77 | * page ID to property value. Otherwise, a multi-dimensional array is |
78 | * returned keyed by page ID, then property name, to property value. |
79 | * |
80 | * An empty array will be returned if no matching properties were found. |
81 | * |
82 | * @param iterable<PageIdentity>|PageIdentity $titles |
83 | * @param string[]|string $propertyNames |
84 | * @return array<int,string|array<string,string>> Keyed by page ID and property name |
85 | * to property value |
86 | */ |
87 | public function getProperties( $titles, $propertyNames ) { |
88 | if ( is_array( $propertyNames ) ) { |
89 | $gotArray = true; |
90 | } else { |
91 | $propertyNames = [ $propertyNames ]; |
92 | $gotArray = false; |
93 | } |
94 | |
95 | $values = []; |
96 | $goodIDs = $this->getGoodIDs( $titles ); |
97 | $queryIDs = []; |
98 | foreach ( $goodIDs as $pageID ) { |
99 | foreach ( $propertyNames as $propertyName ) { |
100 | $propertyValue = $this->getCachedProperty( $pageID, $propertyName ); |
101 | if ( $propertyValue === false ) { |
102 | $queryIDs[] = $pageID; |
103 | break; |
104 | } elseif ( $gotArray ) { |
105 | $values[$pageID][$propertyName] = $propertyValue; |
106 | } else { |
107 | $values[$pageID] = $propertyValue; |
108 | } |
109 | } |
110 | } |
111 | |
112 | if ( $queryIDs ) { |
113 | $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder(); |
114 | $queryBuilder->select( [ 'pp_page', 'pp_propname', 'pp_value' ] ) |
115 | ->from( 'page_props' ) |
116 | ->where( [ 'pp_page' => $queryIDs, 'pp_propname' => $propertyNames ] ) |
117 | ->caller( __METHOD__ ); |
118 | $result = $queryBuilder->fetchResultSet(); |
119 | |
120 | foreach ( $result as $row ) { |
121 | $pageID = $row->pp_page; |
122 | $propertyName = $row->pp_propname; |
123 | $propertyValue = $row->pp_value; |
124 | $this->cache->setField( $pageID, $propertyName, $propertyValue ); |
125 | if ( $gotArray ) { |
126 | $values[$pageID][$propertyName] = $propertyValue; |
127 | } else { |
128 | $values[$pageID] = $propertyValue; |
129 | } |
130 | } |
131 | } |
132 | |
133 | return $values; |
134 | } |
135 | |
136 | /** |
137 | * Get all page properties of one or more page titles. |
138 | * |
139 | * Given one or more Titles, returns an array keyed by page ID to another |
140 | * array from property names to property values. |
141 | * |
142 | * If a single Title is provided without an array, the output will still |
143 | * be returned as an array by page ID. |
144 | * |
145 | * Pages in the provided set of Titles that do have no page properties, |
146 | * will not get a page ID key in the returned array. |
147 | * |
148 | * An empty array will be returned if none of the titles have any page properties. |
149 | * |
150 | * @param iterable<PageIdentity>|PageIdentity $titles |
151 | * @return array<int,array<string,string>> Keyed by page ID and property name to property value |
152 | */ |
153 | public function getAllProperties( $titles ) { |
154 | $values = []; |
155 | $goodIDs = $this->getGoodIDs( $titles ); |
156 | $queryIDs = []; |
157 | foreach ( $goodIDs as $pageID ) { |
158 | $pageProperties = $this->getCachedProperties( $pageID ); |
159 | if ( $pageProperties === false ) { |
160 | $queryIDs[] = $pageID; |
161 | } else { |
162 | $values[$pageID] = $pageProperties; |
163 | } |
164 | } |
165 | |
166 | if ( $queryIDs != [] ) { |
167 | $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder(); |
168 | $queryBuilder->select( [ 'pp_page', 'pp_propname', 'pp_value' ] ) |
169 | ->from( 'page_props' ) |
170 | ->where( [ 'pp_page' => $queryIDs ] ) |
171 | ->caller( __METHOD__ ); |
172 | $result = $queryBuilder->fetchResultSet(); |
173 | |
174 | $currentPageID = 0; |
175 | $pageProperties = []; |
176 | foreach ( $result as $row ) { |
177 | $pageID = $row->pp_page; |
178 | if ( $currentPageID != $pageID ) { |
179 | if ( $pageProperties ) { |
180 | // @phan-suppress-next-line PhanTypeMismatchArgument False positive |
181 | $this->cacheProperties( $currentPageID, $pageProperties ); |
182 | $values[$currentPageID] = $pageProperties; |
183 | } |
184 | $currentPageID = $pageID; |
185 | $pageProperties = []; |
186 | } |
187 | $pageProperties[$row->pp_propname] = $row->pp_value; |
188 | } |
189 | if ( $pageProperties != [] ) { |
190 | // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable pageID set when used |
191 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable pageID set when used |
192 | $this->cacheProperties( $pageID, $pageProperties ); |
193 | // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable pageID set when used |
194 | // @phan-suppress-next-line PhanTypeMismatchDimAssignment pageID set when used |
195 | $values[$pageID] = $pageProperties; |
196 | } |
197 | } |
198 | |
199 | return $values; |
200 | } |
201 | |
202 | /** |
203 | * @param iterable<PageIdentity>|PageIdentity $titles |
204 | * @return int[] List of good page IDs |
205 | */ |
206 | private function getGoodIDs( $titles ) { |
207 | $result = []; |
208 | if ( is_iterable( $titles ) ) { |
209 | if ( $titles instanceof TitleArrayFromResult || |
210 | ( is_array( $titles ) && reset( $titles ) instanceof Title |
211 | ) ) { |
212 | // If the first element is a Title, assume all elements are Titles, |
213 | // and pre-fetch their IDs using a batch query. For PageIdentityValues |
214 | // or PageStoreRecords, this is not necessary, since they already |
215 | // know their ID. |
216 | $this->linkBatchFactory->newLinkBatch( $titles )->execute(); |
217 | } |
218 | |
219 | foreach ( $titles as $title ) { |
220 | // Until we only allow ProperPageIdentity, Title objects |
221 | // can deceive us with an unexpected Special page |
222 | if ( $title->canExist() ) { |
223 | $pageID = $title->getId(); |
224 | if ( $pageID > 0 ) { |
225 | $result[] = $pageID; |
226 | } |
227 | } |
228 | } |
229 | } else { |
230 | // Until we only allow ProperPageIdentity, Title objects |
231 | // can deceive us with an unexpected Special page |
232 | if ( $titles->canExist() ) { |
233 | $pageID = $titles->getId(); |
234 | if ( $pageID > 0 ) { |
235 | $result[] = $pageID; |
236 | } |
237 | } |
238 | } |
239 | return $result; |
240 | } |
241 | |
242 | /** |
243 | * Get a property from the cache. |
244 | * |
245 | * @param int $pageID page ID of page being queried |
246 | * @param string $propertyName name of property being queried |
247 | * @return string|bool property value array or false if not found |
248 | */ |
249 | private function getCachedProperty( $pageID, $propertyName ) { |
250 | if ( $this->cache->hasField( $pageID, $propertyName, self::CACHE_TTL ) ) { |
251 | return $this->cache->getField( $pageID, $propertyName ); |
252 | } |
253 | if ( $this->cache->hasField( 0, $pageID, self::CACHE_TTL ) ) { |
254 | $pageProperties = $this->cache->getField( 0, $pageID ); |
255 | if ( isset( $pageProperties[$propertyName] ) ) { |
256 | return $pageProperties[$propertyName]; |
257 | } |
258 | } |
259 | return false; |
260 | } |
261 | |
262 | /** |
263 | * Get properties from the cache. |
264 | * |
265 | * @param int $pageID page ID of page being queried |
266 | * @return string|bool property value array or false if not found |
267 | */ |
268 | private function getCachedProperties( $pageID ) { |
269 | if ( $this->cache->hasField( 0, $pageID, self::CACHE_TTL ) ) { |
270 | return $this->cache->getField( 0, $pageID ); |
271 | } |
272 | return false; |
273 | } |
274 | |
275 | /** |
276 | * Save properties to the cache. |
277 | * |
278 | * @param int $pageID page ID of page being cached |
279 | * @param string[] $pageProperties associative array of page properties to be cached |
280 | */ |
281 | private function cacheProperties( $pageID, $pageProperties ) { |
282 | $this->cache->clear( $pageID ); |
283 | $this->cache->setField( 0, $pageID, $pageProperties ); |
284 | } |
285 | } |
286 | |
287 | /** @deprecated class alias since 1.40 */ |
288 | class_alias( PageProps::class, 'PageProps' ); |