ApiResult   D
last analyzed

Complexity

Total Complexity 201

Size/Duplication

Total Lines 1171
Duplicated Lines 1.71 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
dl 20
loc 1171
rs 4.4102
c 0
b 0
f 0
wmc 201
lcom 1
cbo 2

39 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 2
A setErrorFormatter() 0 3 1
A serializeForApiResult() 0 3 1
A reset() 0 6 1
A getResultData() 0 16 4
A getSize() 0 3 1
C setValue() 0 37 11
C validateValue() 0 56 14
B addValue() 0 24 6
A unsetValue() 0 8 2
B removeValue() 0 15 5
A setContentValue() 0 7 2
A addContentValue() 0 7 2
A addParsedLimit() 0 5 1
A setContentField() 0 12 4
A addContentField() 0 4 2
A setSubelementsList() 7 7 2
A addSubelementsList() 0 4 1
A unsetSubelementsList() 3 5 2
A removeSubelementsList() 0 4 1
A setIndexedTagName() 0 6 2
A addIndexedTagName() 0 4 1
B setIndexedTagNameRecursive() 0 11 5
A addIndexedTagNameRecursive() 0 4 1
A setPreserveKeysList() 7 7 2
A addPreserveKeysList() 0 4 1
A unsetPreserveKeysList() 3 5 2
A removePreserveKeysList() 0 4 1
A setArrayType() 0 11 3
A addArrayType() 0 4 1
A setArrayTypeRecursive() 0 8 4
A addArrayTypeRecursive() 0 4 1
A isMetadataKey() 0 3 1
F applyTransformations() 0 207 68
C stripMetadata() 0 22 11
C stripMetadataNonRecursive() 0 24 10
B size() 0 14 5
C path() 0 28 7
D addMetadataToResultVars() 0 46 10

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ApiResult often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiResult, and based on these observations, apply Extract Interface, too.

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
/**
22
 * This class represents the result of the API operations.
23
 * It simply wraps a nested array structure, adding some functions to simplify
24
 * array's modifications. As various modules execute, they add different pieces
25
 * of information to this result, structuring it as it will be given to the client.
26
 *
27
 * Each subarray may either be a dictionary - key-value pairs with unique keys,
28
 * or lists, where the items are added using $data[] = $value notation.
29
 *
30
 * @since 1.25 this is no longer a subclass of ApiBase
31
 * @ingroup API
32
 */
33
class ApiResult implements ApiSerializable {
34
35
	/**
36
	 * Override existing value in addValue(), setValue(), and similar functions
37
	 * @since 1.21
38
	 */
39
	const OVERRIDE = 1;
40
41
	/**
42
	 * For addValue(), setValue() and similar functions, if the value does not
43
	 * exist, add it as the first element. In case the new value has no name
44
	 * (numerical index), all indexes will be renumbered.
45
	 * @since 1.21
46
	 */
47
	const ADD_ON_TOP = 2;
48
49
	/**
50
	 * For addValue() and similar functions, do not check size while adding a value
51
	 * Don't use this unless you REALLY know what you're doing.
52
	 * Values added while the size checking was disabled will never be counted.
53
	 * Ignored for setValue() and similar functions.
54
	 * @since 1.24
55
	 */
56
	const NO_SIZE_CHECK = 4;
57
58
	/**
59
	 * For addValue(), setValue() and similar functions, do not validate data.
60
	 * Also disables size checking. If you think you need to use this, you're
61
	 * probably wrong.
62
	 * @since 1.25
63
	 */
64
	const NO_VALIDATE = 12;
65
66
	/**
67
	 * Key for the 'indexed tag name' metadata item. Value is string.
68
	 * @since 1.25
69
	 */
70
	const META_INDEXED_TAG_NAME = '_element';
71
72
	/**
73
	 * Key for the 'subelements' metadata item. Value is string[].
74
	 * @since 1.25
75
	 */
76
	const META_SUBELEMENTS = '_subelements';
77
78
	/**
79
	 * Key for the 'preserve keys' metadata item. Value is string[].
80
	 * @since 1.25
81
	 */
82
	const META_PRESERVE_KEYS = '_preservekeys';
83
84
	/**
85
	 * Key for the 'content' metadata item. Value is string.
86
	 * @since 1.25
87
	 */
88
	const META_CONTENT = '_content';
89
90
	/**
91
	 * Key for the 'type' metadata item. Value is one of the following strings:
92
	 *  - default: Like 'array' if all (non-metadata) keys are numeric with no
93
	 *    gaps, otherwise like 'assoc'.
94
	 *  - array: Keys are used for ordering, but are not output. In a format
95
	 *    like JSON, outputs as [].
96
	 *  - assoc: In a format like JSON, outputs as {}.
97
	 *  - kvp: For a format like XML where object keys have a restricted
98
	 *    character set, use an alternative output format. For example,
99
	 *    <container><item name="key">value</item></container> rather than
100
	 *    <container key="value" />
101
	 *  - BCarray: Like 'array' normally, 'default' in backwards-compatibility mode.
102
	 *  - BCassoc: Like 'assoc' normally, 'default' in backwards-compatibility mode.
103
	 *  - BCkvp: Like 'kvp' normally. In backwards-compatibility mode, forces
104
	 *    the alternative output format for all formats, for example
105
	 *    [{"name":key,"*":value}] in JSON. META_KVP_KEY_NAME must also be set.
106
	 * @since 1.25
107
	 */
108
	const META_TYPE = '_type';
109
110
	/**
111
	 * Key for the metadata item whose value specifies the name used for the
112
	 * kvp key in the alternative output format with META_TYPE 'kvp' or
113
	 * 'BCkvp', i.e. the "name" in <container><item name="key">value</item></container>.
114
	 * Value is string.
115
	 * @since 1.25
116
	 */
117
	const META_KVP_KEY_NAME = '_kvpkeyname';
118
119
	/**
120
	 * Key for the metadata item that indicates that the KVP key should be
121
	 * added into an assoc value, i.e. {"key":{"val1":"a","val2":"b"}}
122
	 * transforms to {"name":"key","val1":"a","val2":"b"} rather than
123
	 * {"name":"key","value":{"val1":"a","val2":"b"}}.
124
	 * Value is boolean.
125
	 * @since 1.26
126
	 */
127
	const META_KVP_MERGE = '_kvpmerge';
128
129
	/**
130
	 * Key for the 'BC bools' metadata item. Value is string[].
131
	 * Note no setter is provided.
132
	 * @since 1.25
133
	 */
134
	const META_BC_BOOLS = '_BC_bools';
135
136
	/**
137
	 * Key for the 'BC subelements' metadata item. Value is string[].
138
	 * Note no setter is provided.
139
	 * @since 1.25
140
	 */
141
	const META_BC_SUBELEMENTS = '_BC_subelements';
142
143
	private $data, $size, $maxSize;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
144
	private $errorFormatter;
145
146
	// Deprecated fields
147
	private $checkingSize, $mainForContinuation;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
148
149
	/**
150
	 * @param int|bool $maxSize Maximum result "size", or false for no limit
151
	 * @since 1.25 Takes an integer|bool rather than an ApiMain
152
	 */
153
	public function __construct( $maxSize ) {
154
		if ( $maxSize instanceof ApiMain ) {
155
			wfDeprecated( 'ApiMain to ' . __METHOD__, '1.25' );
156
			$this->errorFormatter = $maxSize->getErrorFormatter();
157
			$this->mainForContinuation = $maxSize;
158
			$maxSize = $maxSize->getConfig()->get( 'APIMaxResultSize' );
159
		}
160
161
		$this->maxSize = $maxSize;
162
		$this->checkingSize = true;
163
		$this->reset();
164
	}
165
166
	/**
167
	 * Set the error formatter
168
	 * @since 1.25
169
	 * @param ApiErrorFormatter $formatter
170
	 */
171
	public function setErrorFormatter( ApiErrorFormatter $formatter ) {
172
		$this->errorFormatter = $formatter;
173
	}
174
175
	/**
176
	 * Allow for adding one ApiResult into another
177
	 * @since 1.25
178
	 * @return mixed
179
	 */
180
	public function serializeForApiResult() {
181
		return $this->data;
182
	}
183
184
	/************************************************************************//**
185
	 * @name   Content
186
	 * @{
187
	 */
188
189
	/**
190
	 * Clear the current result data.
191
	 */
192
	public function reset() {
193
		$this->data = [
194
			self::META_TYPE => 'assoc', // Usually what's desired
195
		];
196
		$this->size = 0;
197
	}
198
199
	/**
200
	 * Get the result data array
201
	 *
202
	 * The returned value should be considered read-only.
203
	 *
204
	 * Transformations include:
205
	 *
206
	 * Custom: (callable) Applied before other transformations. Signature is
207
	 *  function ( &$data, &$metadata ), return value is ignored. Called for
208
	 *  each nested array.
209
	 *
210
	 * BC: (array) This transformation does various adjustments to bring the
211
	 *  output in line with the pre-1.25 result format. The value array is a
212
	 *  list of flags: 'nobool', 'no*', 'nosub'.
213
	 *  - Boolean-valued items are changed to '' if true or removed if false,
214
	 *    unless listed in META_BC_BOOLS. This may be skipped by including
215
	 *    'nobool' in the value array.
216
	 *  - The tag named by META_CONTENT is renamed to '*', and META_CONTENT is
217
	 *    set to '*'. This may be skipped by including 'no*' in the value
218
	 *    array.
219
	 *  - Tags listed in META_BC_SUBELEMENTS will have their values changed to
220
	 *    [ '*' => $value ]. This may be skipped by including 'nosub' in
221
	 *    the value array.
222
	 *  - If META_TYPE is 'BCarray', set it to 'default'
223
	 *  - If META_TYPE is 'BCassoc', set it to 'default'
224
	 *  - If META_TYPE is 'BCkvp', perform the transformation (even if
225
	 *    the Types transformation is not being applied).
226
	 *
227
	 * Types: (assoc) Apply transformations based on META_TYPE. The values
228
	 * array is an associative array with the following possible keys:
229
	 *  - AssocAsObject: (bool) If true, return arrays with META_TYPE 'assoc'
230
	 *    as objects.
231
	 *  - ArmorKVP: (string) If provided, transform arrays with META_TYPE 'kvp'
232
	 *    and 'BCkvp' into arrays of two-element arrays, something like this:
233
	 *      $output = [];
234
	 *      foreach ( $input as $key => $value ) {
235
	 *          $pair = [];
236
	 *          $pair[$META_KVP_KEY_NAME ?: $ArmorKVP_value] = $key;
237
	 *          ApiResult::setContentValue( $pair, 'value', $value );
238
	 *          $output[] = $pair;
239
	 *      }
240
	 *
241
	 * Strip: (string) Strips metadata keys from the result.
242
	 *  - 'all': Strip all metadata, recursively
243
	 *  - 'base': Strip metadata at the top-level only.
244
	 *  - 'none': Do not strip metadata.
245
	 *  - 'bc': Like 'all', but leave certain pre-1.25 keys.
246
	 *
247
	 * @since 1.25
248
	 * @param array|string|null $path Path to fetch, see ApiResult::addValue
249
	 * @param array $transforms See above
250
	 * @return mixed Result data, or null if not found
251
	 */
252
	public function getResultData( $path = [], $transforms = [] ) {
253
		$path = (array)$path;
254
		if ( !$path ) {
255
			return self::applyTransformations( $this->data, $transforms );
256
		}
257
258
		$last = array_pop( $path );
259
		$ret = &$this->path( $path, 'dummy' );
260
		if ( !isset( $ret[$last] ) ) {
261
			return null;
262
		} elseif ( is_array( $ret[$last] ) ) {
263
			return self::applyTransformations( $ret[$last], $transforms );
264
		} else {
265
			return $ret[$last];
266
		}
267
	}
268
269
	/**
270
	 * Get the size of the result, i.e. the amount of bytes in it
271
	 * @return int
272
	 */
273
	public function getSize() {
274
		return $this->size;
275
	}
276
277
	/**
278
	 * Add an output value to the array by name.
279
	 *
280
	 * Verifies that value with the same name has not been added before.
281
	 *
282
	 * @since 1.25
283
	 * @param array &$arr To add $value to
284
	 * @param string|int|null $name Index of $arr to add $value at,
285
	 *   or null to use the next numeric index.
286
	 * @param mixed $value
287
	 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
288
	 */
289
	public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
290
		if ( ( $flags & ApiResult::NO_VALIDATE ) !== ApiResult::NO_VALIDATE ) {
291
			$value = self::validateValue( $value );
292
		}
293
294
		if ( $name === null ) {
295
			if ( $flags & ApiResult::ADD_ON_TOP ) {
296
				array_unshift( $arr, $value );
297
			} else {
298
				array_push( $arr, $value );
299
			}
300
			return;
301
		}
302
303
		$exists = isset( $arr[$name] );
304
		if ( !$exists || ( $flags & ApiResult::OVERRIDE ) ) {
305
			if ( !$exists && ( $flags & ApiResult::ADD_ON_TOP ) ) {
306
				$arr = [ $name => $value ] + $arr;
307
			} else {
308
				$arr[$name] = $value;
309
			}
310
		} elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
311
			$conflicts = array_intersect_key( $arr[$name], $value );
312
			if ( !$conflicts ) {
313
				$arr[$name] += $value;
314
			} else {
315
				$keys = implode( ', ', array_keys( $conflicts ) );
316
				throw new RuntimeException(
317
					"Conflicting keys ($keys) when attempting to merge element $name"
318
				);
319
			}
320
		} else {
321
			throw new RuntimeException(
322
				"Attempting to add element $name=$value, existing value is {$arr[$name]}"
323
			);
324
		}
325
	}
326
327
	/**
328
	 * Validate a value for addition to the result
329
	 * @param mixed $value
330
	 * @return array|mixed|string
331
	 */
332
	private static function validateValue( $value ) {
333
		global $wgContLang;
334
335
		if ( is_object( $value ) ) {
336
			// Note we use is_callable() here instead of instanceof because
337
			// ApiSerializable is an informal protocol (see docs there for details).
338
			if ( is_callable( [ $value, 'serializeForApiResult' ] ) ) {
339
				$oldValue = $value;
340
				$value = $value->serializeForApiResult();
341
				if ( is_object( $value ) ) {
342
					throw new UnexpectedValueException(
343
						get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' .
344
							get_class( $value )
345
					);
346
				}
347
348
				// Recursive call instead of fall-through so we can throw a
349
				// better exception message.
350
				try {
351
					return self::validateValue( $value );
352
				} catch ( Exception $ex ) {
353
					throw new UnexpectedValueException(
354
						get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' .
355
							$ex->getMessage(),
356
						0,
357
						$ex
358
					);
359
				}
360
			} elseif ( is_callable( [ $value, '__toString' ] ) ) {
361
				$value = (string)$value;
362
			} else {
363
				$value = (array)$value + [ self::META_TYPE => 'assoc' ];
364
			}
365
		}
366
		if ( is_array( $value ) ) {
367
			// Work around PHP bug 45959 by copying to a temporary
368
			// (in this case, foreach gets $k === "1" but $tmp[$k] assigns as if $k === 1)
369
			$tmp = [];
370
			foreach ( $value as $k => $v ) {
371
				$tmp[$k] = self::validateValue( $v );
372
			}
373
			$value = $tmp;
374
		} elseif ( is_float( $value ) && !is_finite( $value ) ) {
375
			throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' );
376
		} elseif ( is_string( $value ) ) {
377
			$value = $wgContLang->normalize( $value );
378
		} elseif ( $value !== null && !is_scalar( $value ) ) {
379
			$type = gettype( $value );
380
			if ( is_resource( $value ) ) {
381
				$type .= '(' . get_resource_type( $value ) . ')';
382
			}
383
			throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
384
		}
385
386
		return $value;
387
	}
388
389
	/**
390
	 * Add value to the output data at the given path.
391
	 *
392
	 * Path can be an indexed array, each element specifying the branch at which to add the new
393
	 * value. Setting $path to [ 'a', 'b', 'c' ] is equivalent to data['a']['b']['c'] = $value.
394
	 * If $path is null, the value will be inserted at the data root.
395
	 *
396
	 * @param array|string|int|null $path
397
	 * @param string|int|null $name See ApiResult::setValue()
398
	 * @param mixed $value
399
	 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
400
	 *   This parameter used to be boolean, and the value of OVERRIDE=1 was specifically
401
	 *   chosen so that it would be backwards compatible with the new method signature.
402
	 * @return bool True if $value fits in the result, false if not
403
	 * @since 1.21 int $flags replaced boolean $override
404
	 */
405
	public function addValue( $path, $name, $value, $flags = 0 ) {
406
		$arr = &$this->path( $path, ( $flags & ApiResult::ADD_ON_TOP ) ? 'prepend' : 'append' );
407
408
		if ( $this->checkingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) {
409
			// self::size needs the validated value. Then flag
410
			// to not re-validate later.
411
			$value = self::validateValue( $value );
412
			$flags |= ApiResult::NO_VALIDATE;
413
414
			$newsize = $this->size + self::size( $value );
415
			if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
416
				/// @todo Add i18n message when replacing calls to ->setWarning()
417
				$msg = new ApiRawMessage( 'This result was truncated because it would otherwise ' .
418
					'be larger than the limit of $1 bytes', 'truncatedresult' );
419
				$msg->numParams( $this->maxSize );
420
				$this->errorFormatter->addWarning( 'result', $msg );
421
				return false;
422
			}
423
			$this->size = $newsize;
424
		}
425
426
		self::setValue( $arr, $name, $value, $flags );
427
		return true;
428
	}
429
430
	/**
431
	 * Remove an output value to the array by name.
432
	 * @param array &$arr To remove $value from
433
	 * @param string|int $name Index of $arr to remove
434
	 * @return mixed Old value, or null
435
	 */
436
	public static function unsetValue( array &$arr, $name ) {
437
		$ret = null;
438
		if ( isset( $arr[$name] ) ) {
439
			$ret = $arr[$name];
440
			unset( $arr[$name] );
441
		}
442
		return $ret;
443
	}
444
445
	/**
446
	 * Remove value from the output data at the given path.
447
	 *
448
	 * @since 1.25
449
	 * @param array|string|null $path See ApiResult::addValue()
450
	 * @param string|int|null $name Index to remove at $path.
451
	 *   If null, $path itself is removed.
452
	 * @param int $flags Flags used when adding the value
453
	 * @return mixed Old value, or null
454
	 */
455
	public function removeValue( $path, $name, $flags = 0 ) {
456
		$path = (array)$path;
457
		if ( $name === null ) {
458
			if ( !$path ) {
459
				throw new InvalidArgumentException( 'Cannot remove the data root' );
460
			}
461
			$name = array_pop( $path );
462
		}
463
		$ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
464
		if ( $this->checkingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) {
465
			$newsize = $this->size - self::size( $ret );
466
			$this->size = max( $newsize, 0 );
467
		}
468
		return $ret;
469
	}
470
471
	/**
472
	 * Add an output value to the array by name and mark as META_CONTENT.
473
	 *
474
	 * @since 1.25
475
	 * @param array &$arr To add $value to
476
	 * @param string|int $name Index of $arr to add $value at.
477
	 * @param mixed $value
478
	 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
479
	 */
480
	public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
481
		if ( $name === null ) {
482
			throw new InvalidArgumentException( 'Content value must be named' );
483
		}
484
		self::setContentField( $arr, $name, $flags );
485
		self::setValue( $arr, $name, $value, $flags );
486
	}
487
488
	/**
489
	 * Add value to the output data at the given path and mark as META_CONTENT
490
	 *
491
	 * @since 1.25
492
	 * @param array|string|null $path See ApiResult::addValue()
493
	 * @param string|int $name See ApiResult::setValue()
494
	 * @param mixed $value
495
	 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
496
	 * @return bool True if $value fits in the result, false if not
497
	 */
498
	public function addContentValue( $path, $name, $value, $flags = 0 ) {
499
		if ( $name === null ) {
500
			throw new InvalidArgumentException( 'Content value must be named' );
501
		}
502
		$this->addContentField( $path, $name, $flags );
503
		$this->addValue( $path, $name, $value, $flags );
504
	}
505
506
	/**
507
	 * Add the numeric limit for a limit=max to the result.
508
	 *
509
	 * @since 1.25
510
	 * @param string $moduleName
511
	 * @param int $limit
512
	 */
513
	public function addParsedLimit( $moduleName, $limit ) {
514
		// Add value, allowing overwriting
515
		$this->addValue( 'limits', $moduleName, $limit,
516
			ApiResult::OVERRIDE | ApiResult::NO_SIZE_CHECK );
517
	}
518
519
	/**@}*/
520
521
	/************************************************************************//**
522
	 * @name   Metadata
523
	 * @{
524
	 */
525
526
	/**
527
	 * Set the name of the content field name (META_CONTENT)
528
	 *
529
	 * @since 1.25
530
	 * @param array &$arr
531
	 * @param string|int $name Name of the field
532
	 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
533
	 */
534
	public static function setContentField( array &$arr, $name, $flags = 0 ) {
535
		if ( isset( $arr[self::META_CONTENT] ) &&
536
			isset( $arr[$arr[self::META_CONTENT]] ) &&
537
			!( $flags & self::OVERRIDE )
538
		) {
539
			throw new RuntimeException(
540
				"Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
541
					' is already set as the content element'
542
			);
543
		}
544
		$arr[self::META_CONTENT] = $name;
545
	}
546
547
	/**
548
	 * Set the name of the content field name (META_CONTENT)
549
	 *
550
	 * @since 1.25
551
	 * @param array|string|null $path See ApiResult::addValue()
552
	 * @param string|int $name Name of the field
553
	 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
554
	 */
555
	public function addContentField( $path, $name, $flags = 0 ) {
556
		$arr = &$this->path( $path, ( $flags & ApiResult::ADD_ON_TOP ) ? 'prepend' : 'append' );
557
		self::setContentField( $arr, $name, $flags );
558
	}
559
560
	/**
561
	 * Causes the elements with the specified names to be output as
562
	 * subelements rather than attributes.
563
	 * @since 1.25 is static
564
	 * @param array &$arr
565
	 * @param array|string|int $names The element name(s) to be output as subelements
566
	 */
567 View Code Duplication
	public static function setSubelementsList( array &$arr, $names ) {
568
		if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
569
			$arr[self::META_SUBELEMENTS] = (array)$names;
570
		} else {
571
			$arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
572
		}
573
	}
574
575
	/**
576
	 * Causes the elements with the specified names to be output as
577
	 * subelements rather than attributes.
578
	 * @since 1.25
579
	 * @param array|string|null $path See ApiResult::addValue()
580
	 * @param array|string|int $names The element name(s) to be output as subelements
581
	 */
582
	public function addSubelementsList( $path, $names ) {
583
		$arr = &$this->path( $path );
584
		self::setSubelementsList( $arr, $names );
585
	}
586
587
	/**
588
	 * Causes the elements with the specified names to be output as
589
	 * attributes (when possible) rather than as subelements.
590
	 * @since 1.25
591
	 * @param array &$arr
592
	 * @param array|string|int $names The element name(s) to not be output as subelements
593
	 */
594
	public static function unsetSubelementsList( array &$arr, $names ) {
595 View Code Duplication
		if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
596
			$arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
597
		}
598
	}
599
600
	/**
601
	 * Causes the elements with the specified names to be output as
602
	 * attributes (when possible) rather than as subelements.
603
	 * @since 1.25
604
	 * @param array|string|null $path See ApiResult::addValue()
605
	 * @param array|string|int $names The element name(s) to not be output as subelements
606
	 */
607
	public function removeSubelementsList( $path, $names ) {
608
		$arr = &$this->path( $path );
609
		self::unsetSubelementsList( $arr, $names );
610
	}
611
612
	/**
613
	 * Set the tag name for numeric-keyed values in XML format
614
	 * @since 1.25 is static
615
	 * @param array &$arr
616
	 * @param string $tag Tag name
617
	 */
618
	public static function setIndexedTagName( array &$arr, $tag ) {
619
		if ( !is_string( $tag ) ) {
620
			throw new InvalidArgumentException( 'Bad tag name' );
621
		}
622
		$arr[self::META_INDEXED_TAG_NAME] = $tag;
623
	}
624
625
	/**
626
	 * Set the tag name for numeric-keyed values in XML format
627
	 * @since 1.25
628
	 * @param array|string|null $path See ApiResult::addValue()
629
	 * @param string $tag Tag name
630
	 */
631
	public function addIndexedTagName( $path, $tag ) {
632
		$arr = &$this->path( $path );
633
		self::setIndexedTagName( $arr, $tag );
634
	}
635
636
	/**
637
	 * Set indexed tag name on $arr and all subarrays
638
	 *
639
	 * @since 1.25
640
	 * @param array &$arr
641
	 * @param string $tag Tag name
642
	 */
643
	public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
644
		if ( !is_string( $tag ) ) {
645
			throw new InvalidArgumentException( 'Bad tag name' );
646
		}
647
		$arr[self::META_INDEXED_TAG_NAME] = $tag;
648
		foreach ( $arr as $k => &$v ) {
649
			if ( !self::isMetadataKey( $k ) && is_array( $v ) ) {
650
				self::setIndexedTagNameRecursive( $v, $tag );
651
			}
652
		}
653
	}
654
655
	/**
656
	 * Set indexed tag name on $path and all subarrays
657
	 *
658
	 * @since 1.25
659
	 * @param array|string|null $path See ApiResult::addValue()
660
	 * @param string $tag Tag name
661
	 */
662
	public function addIndexedTagNameRecursive( $path, $tag ) {
663
		$arr = &$this->path( $path );
664
		self::setIndexedTagNameRecursive( $arr, $tag );
665
	}
666
667
	/**
668
	 * Preserve specified keys.
669
	 *
670
	 * This prevents XML name mangling and preventing keys from being removed
671
	 * by self::stripMetadata().
672
	 *
673
	 * @since 1.25
674
	 * @param array &$arr
675
	 * @param array|string $names The element name(s) to preserve
676
	 */
677 View Code Duplication
	public static function setPreserveKeysList( array &$arr, $names ) {
678
		if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
679
			$arr[self::META_PRESERVE_KEYS] = (array)$names;
680
		} else {
681
			$arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
682
		}
683
	}
684
685
	/**
686
	 * Preserve specified keys.
687
	 * @since 1.25
688
	 * @see self::setPreserveKeysList()
689
	 * @param array|string|null $path See ApiResult::addValue()
690
	 * @param array|string $names The element name(s) to preserve
691
	 */
692
	public function addPreserveKeysList( $path, $names ) {
693
		$arr = &$this->path( $path );
694
		self::setPreserveKeysList( $arr, $names );
695
	}
696
697
	/**
698
	 * Don't preserve specified keys.
699
	 * @since 1.25
700
	 * @see self::setPreserveKeysList()
701
	 * @param array &$arr
702
	 * @param array|string $names The element name(s) to not preserve
703
	 */
704
	public static function unsetPreserveKeysList( array &$arr, $names ) {
705 View Code Duplication
		if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
706
			$arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
707
		}
708
	}
709
710
	/**
711
	 * Don't preserve specified keys.
712
	 * @since 1.25
713
	 * @see self::setPreserveKeysList()
714
	 * @param array|string|null $path See ApiResult::addValue()
715
	 * @param array|string $names The element name(s) to not preserve
716
	 */
717
	public function removePreserveKeysList( $path, $names ) {
718
		$arr = &$this->path( $path );
719
		self::unsetPreserveKeysList( $arr, $names );
720
	}
721
722
	/**
723
	 * Set the array data type
724
	 *
725
	 * @since 1.25
726
	 * @param array &$arr
727
	 * @param string $type See ApiResult::META_TYPE
728
	 * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME
729
	 */
730
	public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
731
		if ( !in_array( $type, [
732
				'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
733
				], true ) ) {
734
			throw new InvalidArgumentException( 'Bad type' );
735
		}
736
		$arr[self::META_TYPE] = $type;
737
		if ( is_string( $kvpKeyName ) ) {
738
			$arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
739
		}
740
	}
741
742
	/**
743
	 * Set the array data type for a path
744
	 * @since 1.25
745
	 * @param array|string|null $path See ApiResult::addValue()
746
	 * @param string $tag See ApiResult::META_TYPE
747
	 * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME
748
	 */
749
	public function addArrayType( $path, $tag, $kvpKeyName = null ) {
750
		$arr = &$this->path( $path );
751
		self::setArrayType( $arr, $tag, $kvpKeyName );
752
	}
753
754
	/**
755
	 * Set the array data type recursively
756
	 * @since 1.25
757
	 * @param array &$arr
758
	 * @param string $type See ApiResult::META_TYPE
759
	 * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME
760
	 */
761
	public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) {
762
		self::setArrayType( $arr, $type, $kvpKeyName );
763
		foreach ( $arr as $k => &$v ) {
764
			if ( !self::isMetadataKey( $k ) && is_array( $v ) ) {
765
				self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
766
			}
767
		}
768
	}
769
770
	/**
771
	 * Set the array data type for a path recursively
772
	 * @since 1.25
773
	 * @param array|string|null $path See ApiResult::addValue()
774
	 * @param string $tag See ApiResult::META_TYPE
775
	 * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME
776
	 */
777
	public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
778
		$arr = &$this->path( $path );
779
		self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
780
	}
781
782
	/**@}*/
783
784
	/************************************************************************//**
785
	 * @name   Utility
786
	 * @{
787
	 */
788
789
	/**
790
	 * Test whether a key should be considered metadata
791
	 *
792
	 * @param string $key
793
	 * @return bool
794
	 */
795
	public static function isMetadataKey( $key ) {
796
		return substr( $key, 0, 1 ) === '_';
797
	}
798
799
	/**
800
	 * Apply transformations to an array, returning the transformed array.
801
	 *
802
	 * @see ApiResult::getResultData()
803
	 * @since 1.25
804
	 * @param array $dataIn
805
	 * @param array $transforms
806
	 * @return array|object
807
	 */
808
	protected static function applyTransformations( array $dataIn, array $transforms ) {
809
		$strip = isset( $transforms['Strip'] ) ? $transforms['Strip'] : 'none';
810
		if ( $strip === 'base' ) {
811
			$transforms['Strip'] = 'none';
812
		}
813
		$transformTypes = isset( $transforms['Types'] ) ? $transforms['Types'] : null;
814
		if ( $transformTypes !== null && !is_array( $transformTypes ) ) {
815
			throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' );
816
		}
817
818
		$metadata = [];
819
		$data = self::stripMetadataNonRecursive( $dataIn, $metadata );
820
821
		if ( isset( $transforms['Custom'] ) ) {
822
			if ( !is_callable( $transforms['Custom'] ) ) {
823
				throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' );
824
			}
825
			call_user_func_array( $transforms['Custom'], [ &$data, &$metadata ] );
826
		}
827
828
		if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) &&
829
			isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' &&
830
			!isset( $metadata[self::META_KVP_KEY_NAME] )
831
		) {
832
			throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' .
833
				'ApiResult::META_KVP_KEY_NAME metadata item' );
834
		}
835
836
		// BC transformations
837
		$boolKeys = null;
838
		if ( isset( $transforms['BC'] ) ) {
839
			if ( !is_array( $transforms['BC'] ) ) {
840
				throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' );
841
			}
842
			if ( !in_array( 'nobool', $transforms['BC'], true ) ) {
843
				$boolKeys = isset( $metadata[self::META_BC_BOOLS] )
844
					? array_flip( $metadata[self::META_BC_BOOLS] )
845
					: [];
846
			}
847
848
			if ( !in_array( 'no*', $transforms['BC'], true ) &&
849
				isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*'
850
			) {
851
				$k = $metadata[self::META_CONTENT];
852
				$data['*'] = $data[$k];
853
				unset( $data[$k] );
854
				$metadata[self::META_CONTENT] = '*';
855
			}
856
857
			if ( !in_array( 'nosub', $transforms['BC'], true ) &&
858
				isset( $metadata[self::META_BC_SUBELEMENTS] )
859
			) {
860
				foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) {
861
					if ( isset( $data[$k] ) ) {
862
						$data[$k] = [
863
							'*' => $data[$k],
864
							self::META_CONTENT => '*',
865
							self::META_TYPE => 'assoc',
866
						];
867
					}
868
				}
869
			}
870
871
			if ( isset( $metadata[self::META_TYPE] ) ) {
872
				switch ( $metadata[self::META_TYPE] ) {
873
					case 'BCarray':
874
					case 'BCassoc':
875
						$metadata[self::META_TYPE] = 'default';
876
						break;
877
					case 'BCkvp':
878
						$transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME];
879
						break;
880
				}
881
			}
882
		}
883
884
		// Figure out type, do recursive calls, and do boolean transform if necessary
885
		$defaultType = 'array';
886
		$maxKey = -1;
887
		foreach ( $data as $k => &$v ) {
888
			$v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v;
889
			if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) {
890
				if ( !$v ) {
891
					unset( $data[$k] );
892
					continue;
893
				}
894
				$v = '';
895
			}
896
			if ( is_string( $k ) ) {
897
				$defaultType = 'assoc';
898
			} elseif ( $k > $maxKey ) {
899
				$maxKey = $k;
900
			}
901
		}
902
		unset( $v );
903
904
		// Determine which metadata to keep
905
		switch ( $strip ) {
906
			case 'all':
907
			case 'base':
908
				$keepMetadata = [];
909
				break;
910
			case 'none':
911
				$keepMetadata = &$metadata;
912
				break;
913
			case 'bc':
914
				$keepMetadata = array_intersect_key( $metadata, [
915
					self::META_INDEXED_TAG_NAME => 1,
916
					self::META_SUBELEMENTS => 1,
917
				] );
918
				break;
919
			default:
920
				throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' );
921
		}
922
923
		// Type transformation
924
		if ( $transformTypes !== null ) {
925
			if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) {
926
				$defaultType = 'assoc';
927
			}
928
929
			// Override type, if provided
930
			$type = $defaultType;
931
			if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) {
932
				$type = $metadata[self::META_TYPE];
933
			}
934
			if ( ( $type === 'kvp' || $type === 'BCkvp' ) &&
935
				empty( $transformTypes['ArmorKVP'] )
936
			) {
937
				$type = 'assoc';
938
			} elseif ( $type === 'BCarray' ) {
939
				$type = 'array';
940
			} elseif ( $type === 'BCassoc' ) {
941
				$type = 'assoc';
942
			}
943
944
			// Apply transformation
945
			switch ( $type ) {
946
				case 'assoc':
947
					$metadata[self::META_TYPE] = 'assoc';
948
					$data += $keepMetadata;
949
					return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data;
950
951
				case 'array':
952
					ksort( $data );
953
					$data = array_values( $data );
954
					$metadata[self::META_TYPE] = 'array';
955
					return $data + $keepMetadata;
956
957
				case 'kvp':
958
				case 'BCkvp':
959
					$key = isset( $metadata[self::META_KVP_KEY_NAME] )
960
						? $metadata[self::META_KVP_KEY_NAME]
961
						: $transformTypes['ArmorKVP'];
962
					$valKey = isset( $transforms['BC'] ) ? '*' : 'value';
963
					$assocAsObject = !empty( $transformTypes['AssocAsObject'] );
964
					$merge = !empty( $metadata[self::META_KVP_MERGE] );
965
966
					$ret = [];
967
					foreach ( $data as $k => $v ) {
968
						if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
969
							$vArr = (array)$v;
970
							if ( isset( $vArr[self::META_TYPE] ) ) {
971
								$mergeType = $vArr[self::META_TYPE];
972
							} elseif ( is_object( $v ) ) {
973
								$mergeType = 'assoc';
974
							} else {
975
								$keys = array_keys( $vArr );
976
								sort( $keys, SORT_NUMERIC );
977
								$mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
978
							}
979
						} else {
980
							$mergeType = 'n/a';
981
						}
982
						if ( $mergeType === 'assoc' ) {
983
							$item = $vArr + [
0 ignored issues
show
Bug introduced by
The variable $vArr 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...
984
								$key => $k,
985
							];
986
							if ( $strip === 'none' ) {
987
								self::setPreserveKeysList( $item, [ $key ] );
988
							}
989
						} else {
990
							$item = [
991
								$key => $k,
992
								$valKey => $v,
993
							];
994
							if ( $strip === 'none' ) {
995
								$item += [
996
									self::META_PRESERVE_KEYS => [ $key ],
997
									self::META_CONTENT => $valKey,
998
									self::META_TYPE => 'assoc',
999
								];
1000
							}
1001
						}
1002
						$ret[] = $assocAsObject ? (object)$item : $item;
1003
					}
1004
					$metadata[self::META_TYPE] = 'array';
1005
1006
					return $ret + $keepMetadata;
1007
1008
				default:
1009
					throw new UnexpectedValueException( "Unknown type '$type'" );
1010
			}
1011
		} else {
1012
			return $data + $keepMetadata;
1013
		}
1014
	}
1015
1016
	/**
1017
	 * Recursively remove metadata keys from a data array or object
1018
	 *
1019
	 * Note this removes all potential metadata keys, not just the defined
1020
	 * ones.
1021
	 *
1022
	 * @since 1.25
1023
	 * @param array|object $data
1024
	 * @return array|object
1025
	 */
1026
	public static function stripMetadata( $data ) {
1027
		if ( is_array( $data ) || is_object( $data ) ) {
1028
			$isObj = is_object( $data );
1029
			if ( $isObj ) {
1030
				$data = (array)$data;
1031
			}
1032
			$preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1033
				? (array)$data[self::META_PRESERVE_KEYS]
1034
				: [];
1035
			foreach ( $data as $k => $v ) {
1036
				if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1037
					unset( $data[$k] );
1038
				} elseif ( is_array( $v ) || is_object( $v ) ) {
1039
					$data[$k] = self::stripMetadata( $v );
1040
				}
1041
			}
1042
			if ( $isObj ) {
1043
				$data = (object)$data;
1044
			}
1045
		}
1046
		return $data;
1047
	}
1048
1049
	/**
1050
	 * Remove metadata keys from a data array or object, non-recursive
1051
	 *
1052
	 * Note this removes all potential metadata keys, not just the defined
1053
	 * ones.
1054
	 *
1055
	 * @since 1.25
1056
	 * @param array|object $data
1057
	 * @param array &$metadata Store metadata here, if provided
1058
	 * @return array|object
1059
	 */
1060
	public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1061
		if ( !is_array( $metadata ) ) {
1062
			$metadata = [];
1063
		}
1064
		if ( is_array( $data ) || is_object( $data ) ) {
1065
			$isObj = is_object( $data );
1066
			if ( $isObj ) {
1067
				$data = (array)$data;
1068
			}
1069
			$preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1070
				? (array)$data[self::META_PRESERVE_KEYS]
1071
				: [];
1072
			foreach ( $data as $k => $v ) {
1073
				if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1074
					$metadata[$k] = $v;
1075
					unset( $data[$k] );
1076
				}
1077
			}
1078
			if ( $isObj ) {
1079
				$data = (object)$data;
1080
			}
1081
		}
1082
		return $data;
1083
	}
1084
1085
	/**
1086
	 * Get the 'real' size of a result item. This means the strlen() of the item,
1087
	 * or the sum of the strlen()s of the elements if the item is an array.
1088
	 * @param mixed $value Validated value (see self::validateValue())
1089
	 * @return int
1090
	 */
1091
	private static function size( $value ) {
1092
		$s = 0;
1093
		if ( is_array( $value ) ) {
1094
			foreach ( $value as $k => $v ) {
1095
				if ( !self::isMetadataKey( $k ) ) {
1096
					$s += self::size( $v );
1097
				}
1098
			}
1099
		} elseif ( is_scalar( $value ) ) {
1100
			$s = strlen( $value );
1101
		}
1102
1103
		return $s;
1104
	}
1105
1106
	/**
1107
	 * Return a reference to the internal data at $path
1108
	 *
1109
	 * @param array|string|null $path
1110
	 * @param string $create
1111
	 *   If 'append', append empty arrays.
1112
	 *   If 'prepend', prepend empty arrays.
1113
	 *   If 'dummy', return a dummy array.
1114
	 *   Else, raise an error.
1115
	 * @return array
1116
	 */
1117
	private function &path( $path, $create = 'append' ) {
1118
		$path = (array)$path;
1119
		$ret = &$this->data;
1120
		foreach ( $path as $i => $k ) {
1121
			if ( !isset( $ret[$k] ) ) {
1122
				switch ( $create ) {
1123
					case 'append':
1124
						$ret[$k] = [];
1125
						break;
1126
					case 'prepend':
1127
						$ret = [ $k => [] ] + $ret;
1128
						break;
1129
					case 'dummy':
1130
						$tmp = [];
1131
						return $tmp;
1132
					default:
1133
						$fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1134
						throw new InvalidArgumentException( "Path $fail does not exist" );
1135
				}
1136
			}
1137
			if ( !is_array( $ret[$k] ) ) {
1138
				$fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1139
				throw new InvalidArgumentException( "Path $fail is not an array" );
1140
			}
1141
			$ret = &$ret[$k];
1142
		}
1143
		return $ret;
1144
	}
1145
1146
	/**
1147
	 * Add the correct metadata to an array of vars we want to export through
1148
	 * the API.
1149
	 *
1150
	 * @param array $vars
1151
	 * @param bool $forceHash
1152
	 * @return array
1153
	 */
1154
	public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1155
		// Process subarrays and determine if this is a JS [] or {}
1156
		$hash = $forceHash;
1157
		$maxKey = -1;
1158
		$bools = [];
1159
		foreach ( $vars as $k => $v ) {
1160
			if ( is_array( $v ) || is_object( $v ) ) {
1161
				$vars[$k] = ApiResult::addMetadataToResultVars( (array)$v, is_object( $v ) );
1162
			} elseif ( is_bool( $v ) ) {
1163
				// Better here to use real bools even in BC formats
1164
				$bools[] = $k;
1165
			}
1166
			if ( is_string( $k ) ) {
1167
				$hash = true;
1168
			} elseif ( $k > $maxKey ) {
1169
				$maxKey = $k;
1170
			}
1171
		}
1172
		if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1173
			$hash = true;
1174
		}
1175
1176
		// Set metadata appropriately
1177
		if ( $hash ) {
1178
			// Get the list of keys we actually care about. Unfortunately, we can't support
1179
			// certain keys that conflict with ApiResult metadata.
1180
			$keys = array_diff( array_keys( $vars ), [
1181
				ApiResult::META_TYPE, ApiResult::META_PRESERVE_KEYS, ApiResult::META_KVP_KEY_NAME,
1182
				ApiResult::META_INDEXED_TAG_NAME, ApiResult::META_BC_BOOLS
1183
			] );
1184
1185
			return [
1186
				ApiResult::META_TYPE => 'kvp',
1187
				ApiResult::META_KVP_KEY_NAME => 'key',
1188
				ApiResult::META_PRESERVE_KEYS => $keys,
1189
				ApiResult::META_BC_BOOLS => $bools,
1190
				ApiResult::META_INDEXED_TAG_NAME => 'var',
1191
			] + $vars;
1192
		} else {
1193
			return [
1194
				ApiResult::META_TYPE => 'array',
1195
				ApiResult::META_BC_BOOLS => $bools,
1196
				ApiResult::META_INDEXED_TAG_NAME => 'value',
1197
			] + $vars;
1198
		}
1199
	}
1200
1201
	/**@}*/
1202
1203
}
1204
1205
/**
1206
 * For really cool vim folding this needs to be at the end:
1207
 * vim: foldmarker=@{,@} foldmethod=marker
1208
 */
1209