Completed
Branch master (0c9f05)
by
unknown
29:21
created

PageProps::ensureCacheSize()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 5
rs 9.4285
c 1
b 0
f 0
1
<?php
2
/**
3
 * Access to properties of a page.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
/**
24
 * Gives access to properties of a page.
25
 *
26
 * @since 1.27
27
 *
28
 */
29
class PageProps {
0 ignored issues
show
Coding Style introduced by
Since you have declared the constructor as private, maybe you should also declare the class as final.
Loading history...
30
31
	/**
32
	 * @var PageProps
33
	 */
34
	private static $instance;
35
36
	/**
37
	 * Overrides the default instance of this class
38
	 * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
39
	 *
40
	 * If this method is used it MUST also be called with null after a test to ensure a new
41
	 * default instance is created next time getInstance is called.
42
	 *
43
	 * @since 1.27
44
	 *
45
	 * @param PageProps|null $store
46
	 *
47
	 * @return ScopedCallback to reset the overridden value
48
	 * @throws MWException
49
	 */
50
	public static function overrideInstance( PageProps $store = null ) {
51
		if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
52
			throw new MWException(
53
				'Cannot override ' . __CLASS__ . 'default instance in operation.'
54
			);
55
		}
56
		$previousValue = self::$instance;
57
		self::$instance = $store;
58
		return new ScopedCallback( function() use ( $previousValue ) {
59
			self::$instance = $previousValue;
60
		} );
61
	}
62
63
	/**
64
	 * @return PageProps
65
	 */
66
	public static function getInstance() {
67
		if ( self::$instance === null ) {
68
			self::$instance = new self();
69
		}
70
		return self::$instance;
71
	}
72
73
	/** Cache parameters */
74
	const CACHE_TTL = 10; // integer; TTL in seconds
75
	const CACHE_SIZE = 100; // integer; max cached pages
76
77
	/** Property cache */
78
	private $cache = null;
79
80
	/**
81
	 * Create a PageProps object
82
	 */
83
	private function __construct() {
84
		$this->cache = new ProcessCacheLRU( self::CACHE_SIZE );
85
	}
86
87
	/**
88
	 * Ensure that cache has at least this size
89
	 * @param int $size
90
	 */
91
	public function ensureCacheSize( $size ) {
92
		if ( $this->cache->getSize() < $size ) {
93
			$this->cache->resize( $size );
94
		}
95
	}
96
97
	/**
98
	 * Given one or more Titles and one or more names of properties,
99
	 * returns an associative array mapping page ID to property value.
100
	 * Pages in the provided set of Titles that do not have a value for
101
	 * the given properties will not appear in the returned array. If a
102
	 * single Title is provided, it does not need to be passed in an array,
103
	 * but an array will always be returned. If a single property name is
104
	 * provided, it does not need to be passed in an array. In that case,
105
	 * an associative array mapping page ID to property value will be
106
	 * returned; otherwise, an associative array mapping page ID to
107
	 * an associative array mapping property name to property value will be
108
	 * returned. An empty array will be returned if no matching properties
109
	 * were found.
110
	 *
111
	 * @param Title[]|Title $titles
112
	 * @param string[]|string $propertyNames
113
	 * @return array associative array mapping page ID to property value
114
	 */
115
	public function getProperties( $titles, $propertyNames ) {
116
		if ( is_array( $propertyNames ) ) {
117
			$gotArray = true;
118
		} else {
119
			$propertyNames = [ $propertyNames ];
120
			$gotArray = false;
121
		}
122
123
		$values = [];
124
		$goodIDs = $this->getGoodIDs( $titles );
125
		$queryIDs = [];
126
		foreach ( $goodIDs as $pageID ) {
127
			foreach ( $propertyNames as $propertyName ) {
128
				$propertyValue = $this->getCachedProperty( $pageID, $propertyName );
129
				if ( $propertyValue === false ) {
130
					$queryIDs[] = $pageID;
131
					break;
132
				} else {
133
					if ( $gotArray ) {
134
						$values[$pageID][$propertyName] = $propertyValue;
135
					} else {
136
						$values[$pageID] = $propertyValue;
137
					}
138
				}
139
			}
140
		}
141
142
		if ( $queryIDs ) {
143
			$dbr = wfGetDB( DB_SLAVE );
144
			$result = $dbr->select(
145
				'page_props',
146
				[
147
					'pp_page',
148
					'pp_propname',
149
					'pp_value'
150
				],
151
				[
152
					'pp_page' => $queryIDs,
153
					'pp_propname' => $propertyNames
154
				],
155
				__METHOD__
156
			);
157
158
			foreach ( $result as $row ) {
0 ignored issues
show
Bug introduced by
The expression $result of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
159
				$pageID = $row->pp_page;
160
				$propertyName = $row->pp_propname;
161
				$propertyValue = $row->pp_value;
162
				$this->cacheProperty( $pageID, $propertyName, $propertyValue );
163
				if ( $gotArray ) {
164
					$values[$pageID][$propertyName] = $propertyValue;
165
				} else {
166
					$values[$pageID] = $propertyValue;
167
				}
168
			}
169
		}
170
171
		return $values;
172
	}
173
174
	/**
175
	 * Get all page property values.
176
	 * Given one or more Titles, returns an associative array mapping page
177
	 * ID to an associative array mapping property names to property
178
	 * values. Pages in the provided set of Titles that do not have any
179
	 * properties will not appear in the returned array. If a single Title
180
	 * is provided, it does not need to be passed in an array, but an array
181
	 * will always be returned. An empty array will be returned if no
182
	 * matching properties were found.
183
	 *
184
	 * @param Title[]|Title $titles
185
	 * @return array associative array mapping page ID to property value array
186
	 */
187
	public function getAllProperties( $titles ) {
188
		$values = [];
189
		$goodIDs = $this->getGoodIDs( $titles );
190
		$queryIDs = [];
191
		foreach ( $goodIDs as $pageID ) {
192
			$pageProperties = $this->getCachedProperties( $pageID );
193
			if ( $pageProperties === false ) {
194
				$queryIDs[] = $pageID;
195
			} else {
196
				$values[$pageID] = $pageProperties;
197
			}
198
		}
199
200
		if ( $queryIDs != [] ) {
201
			$dbr = wfGetDB( DB_SLAVE );
202
			$result = $dbr->select(
203
				'page_props',
204
				[
205
					'pp_page',
206
					'pp_propname',
207
					'pp_value'
208
				],
209
				[
210
					'pp_page' => $queryIDs,
211
				],
212
				__METHOD__
213
			);
214
215
			$currentPageID = 0;
216
			$pageProperties = [];
217
			foreach ( $result as $row ) {
0 ignored issues
show
Bug introduced by
The expression $result of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
218
				$pageID = $row->pp_page;
219
				if ( $currentPageID != $pageID ) {
220
					if ( $pageProperties != [] ) {
221
						$this->cacheProperties( $currentPageID, $pageProperties );
222
						$values[$currentPageID] = $pageProperties;
223
					}
224
					$currentPageID = $pageID;
225
					$pageProperties = [];
226
				}
227
				$pageProperties[$row->pp_propname] = $row->pp_value;
228
			}
229
			if ( $pageProperties != [] ) {
230
				$this->cacheProperties( $pageID, $pageProperties );
0 ignored issues
show
Bug introduced by
The variable $pageID does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
231
				$values[$pageID] = $pageProperties;
232
			}
233
		}
234
235
		return $values;
236
	}
237
238
	/**
239
	 * @param Title[]|Title $titles
240
	 * @return array array of good page IDs
241
	 */
242
	private function getGoodIDs( $titles ) {
243
		$result = [];
244
		if ( is_array( $titles ) ) {
245
			foreach ( $titles as $title ) {
246
				$pageID = $title->getArticleID();
247
				if ( $pageID > 0 ) {
248
					$result[] = $pageID;
249
				}
250
			}
251
		} else {
252
			$pageID = $titles->getArticleID();
253
			if ( $pageID > 0 ) {
254
				$result[] = $pageID;
255
			}
256
		}
257
		return $result;
258
	}
259
260
	/**
261
	 * Get a property from the cache.
262
	 *
263
	 * @param int $pageID page ID of page being queried
264
	 * @param string $propertyName name of property being queried
265
	 * @return string|bool property value array or false if not found
266
	 */
267
	private function getCachedProperty( $pageID, $propertyName ) {
268
		if ( $this->cache->has( $pageID, $propertyName, self::CACHE_TTL ) ) {
269
			return $this->cache->get( $pageID, $propertyName );
270
		}
271
		if ( $this->cache->has( 0, $pageID, self::CACHE_TTL ) ) {
272
			$pageProperties = $this->cache->get( 0, $pageID );
273
			if ( isset( $pageProperties[$propertyName] ) ) {
274
				return $pageProperties[$propertyName];
275
			}
276
		}
277
		return false;
278
	}
279
280
	/**
281
	 * Get properties from the cache.
282
	 *
283
	 * @param int $pageID page ID of page being queried
284
	 * @return string|bool property value array or false if not found
285
	 */
286
	private function getCachedProperties( $pageID ) {
287
		if ( $this->cache->has( 0, $pageID, self::CACHE_TTL ) ) {
288
			return $this->cache->get( 0, $pageID );
289
		}
290
		return false;
291
	}
292
293
	/**
294
	 * Save a property to the cache.
295
	 *
296
	 * @param int $pageID page ID of page being cached
297
	 * @param string $propertyName name of property being cached
298
	 * @param mixed $propertyValue value of property
299
	 */
300
	private function cacheProperty( $pageID, $propertyName, $propertyValue ) {
301
		$this->cache->set( $pageID, $propertyName, $propertyValue );
302
	}
303
304
	/**
305
	 * Save properties to the cache.
306
	 *
307
	 * @param int $pageID page ID of page being cached
308
	 * @param string[] $pageProperties associative array of page properties to be cached
309
	 */
310
	private function cacheProperties( $pageID, $pageProperties ) {
311
		$this->cache->clear( $pageID );
312
		$this->cache->set( 0, $pageID, $pageProperties );
313
	}
314
}
315