Completed
Branch master (86dc85)
by
unknown
23:45
created

PageProps::overrideInstance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 12
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 12
loc 12
rs 9.4285
cc 2
eloc 8
nc 2
nop 1
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 View Code Duplication
	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
	 * Given one or more Titles and one or more names of properties,
89
	 * returns an associative array mapping page ID to property value.
90
	 * Pages in the provided set of Titles that do not have a value for
91
	 * the given properties will not appear in the returned array. If a
92
	 * single Title is provided, it does not need to be passed in an array,
93
	 * but an array will always be returned. If a single property name is
94
	 * provided, it does not need to be passed in an array. In that case,
95
	 * an associtive array mapping page ID to property value will be
96
	 * returned; otherwise, an associative array mapping page ID to
97
	 * an associative array mapping property name to property value will be
98
	 * returned. An empty array will be returned if no matching properties
99
	 * were found.
100
	 *
101
	 * @param Title[]|Title $titles
102
	 * @param string[]|string $propertyNames
103
	 * @return array associative array mapping page ID to property value
104
	 */
105
	public function getProperties( $titles, $propertyNames ) {
106
		if ( is_array( $propertyNames ) ) {
107
			$gotArray = true;
108
		} else {
109
			$propertyNames = [ $propertyNames ];
110
			$gotArray = false;
111
		}
112
113
		$values = [];
114
		$goodIDs = $this->getGoodIDs( $titles );
115
		$queryIDs = [];
116
		foreach ( $goodIDs as $pageID ) {
117
			foreach ( $propertyNames as $propertyName ) {
118
				$propertyValue = $this->getCachedProperty( $pageID, $propertyName );
119
				if ( $propertyValue === false ) {
120
					$queryIDs[] = $pageID;
121
					break;
122
				} else {
123
					if ( $gotArray ) {
124
						$values[$pageID][$propertyName] = $propertyValue;
125
					} else {
126
						$values[$pageID] = $propertyValue;
127
					}
128
				}
129
			}
130
		}
131
132
		if ( $queryIDs ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $queryIDs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
133
			$dbr = wfGetDB( DB_SLAVE );
134
			$result = $dbr->select(
135
				'page_props',
136
				[
137
					'pp_page',
138
					'pp_propname',
139
					'pp_value'
140
				],
141
				[
142
					'pp_page' => $queryIDs,
143
					'pp_propname' => $propertyNames
144
				],
145
				__METHOD__
146
			);
147
148
			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...
149
				$pageID = $row->pp_page;
150
				$propertyName = $row->pp_propname;
151
				$propertyValue = $row->pp_value;
152
				$this->cacheProperty( $pageID, $propertyName, $propertyValue );
153
				if ( $gotArray ) {
154
					$values[$pageID][$propertyName] = $propertyValue;
155
				} else {
156
					$values[$pageID] = $propertyValue;
157
				}
158
			}
159
		}
160
161
		return $values;
162
	}
163
164
	/**
165
	 * Get all page property values.
166
	 * Given one or more Titles, returns an associative array mapping page
167
	 * ID to an associative array mapping property names to property
168
	 * values. Pages in the provided set of Titles that do not have any
169
	 * properties will not appear in the returned array. If a single Title
170
	 * is provided, it does not need to be passed in an array, but an array
171
	 * will always be returned. An empty array will be returned if no
172
	 * matching properties were found.
173
	 *
174
	 * @param Title[]|Title $titles
175
	 * @return array associative array mapping page ID to property value array
176
	 */
177
	public function getAllProperties( $titles ) {
178
		$values = [];
179
		$goodIDs = $this->getGoodIDs( $titles );
180
		$queryIDs = [];
181
		foreach ( $goodIDs as $pageID ) {
182
			$pageProperties = $this->getCachedProperties( $pageID );
183
			if ( $pageProperties === false ) {
184
				$queryIDs[] = $pageID;
185
			} else {
186
				$values[$pageID] = $pageProperties;
187
			}
188
		}
189
190
		if ( $queryIDs != [] ) {
191
			$dbr = wfGetDB( DB_SLAVE );
192
			$result = $dbr->select(
193
				'page_props',
194
				[
195
					'pp_page',
196
					'pp_propname',
197
					'pp_value'
198
				],
199
				[
200
					'pp_page' => $queryIDs,
201
				],
202
				__METHOD__
203
			);
204
205
			$currentPageID = 0;
206
			$pageProperties = [];
207
			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...
208
				$pageID = $row->pp_page;
209
				if ( $currentPageID != $pageID ) {
210
					if ( $pageProperties != [] ) {
211
						$this->cacheProperties( $currentPageID, $pageProperties );
212
						$values[$currentPageID] = $pageProperties;
213
					}
214
					$currentPageID = $pageID;
215
					$pageProperties = [];
216
				}
217
				$pageProperties[$row->pp_propname] = $row->pp_value;
218
			}
219
			if ( $pageProperties != [] ) {
220
				$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...
221
				$values[$pageID] = $pageProperties;
222
			}
223
		}
224
225
		return $values;
226
	}
227
228
	/**
229
	 * @param Title[]|Title $titles
230
	 * @return array array of good page IDs
231
	 */
232
	private function getGoodIDs( $titles ) {
233
		$result = [];
234
		if ( is_array( $titles ) ) {
235
			foreach ( $titles as $title ) {
236
				$pageID = $title->getArticleID();
237
				if ( $pageID > 0 ) {
238
					$result[] = $pageID;
239
				}
240
			}
241
		} else {
242
			$pageID = $titles->getArticleID();
243
			if ( $pageID > 0 ) {
244
				$result[] = $pageID;
245
			}
246
		}
247
		return $result;
248
	}
249
250
	/**
251
	 * Get a property from the cache.
252
	 *
253
	 * @param int $pageID page ID of page being queried
254
	 * @param string $propertyName name of property being queried
255
	 * @return string|bool property value array or false if not found
256
	 */
257
	private function getCachedProperty( $pageID, $propertyName ) {
258
		if ( $this->cache->has( $pageID, $propertyName, self::CACHE_TTL ) ) {
259
			return $this->cache->get( $pageID, $propertyName );
260
		}
261
		if ( $this->cache->has( 0, $pageID, self::CACHE_TTL ) ) {
262
			$pageProperties = $this->cache->get( 0, $pageID );
263
			if ( isset( $pageProperties[$propertyName] ) ) {
264
				return $pageProperties[$propertyName];
265
			}
266
		}
267
		return false;
268
	}
269
270
	/**
271
	 * Get properties from the cache.
272
	 *
273
	 * @param int $pageID page ID of page being queried
274
	 * @return string|bool property value array or false if not found
275
	 */
276
	private function getCachedProperties( $pageID ) {
277
		if ( $this->cache->has( 0, $pageID, self::CACHE_TTL ) ) {
278
			return $this->cache->get( 0, $pageID );
279
		}
280
		return false;
281
	}
282
283
	/**
284
	 * Save a property to the cache.
285
	 *
286
	 * @param int $pageID page ID of page being cached
287
	 * @param string $propertyName name of property being cached
288
	 * @param mixed $propertyValue value of property
289
	 */
290
	private function cacheProperty( $pageID, $propertyName, $propertyValue ) {
291
		$this->cache->set( $pageID, $propertyName, $propertyValue );
292
	}
293
294
	/**
295
	 * Save properties to the cache.
296
	 *
297
	 * @param int $pageID page ID of page being cached
298
	 * @param string[] $pageProperties associative array of page properties to be cached
299
	 */
300
	private function cacheProperties( $pageID, $pageProperties ) {
301
		$this->cache->clear( $pageID );
302
		$this->cache->set( 0, $pageID, $pageProperties );
303
	}
304
}
305