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