Passed
Push — objectById ( 934c73 )
by no
03:49
created

ByPropertyIdArray   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 451
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 6
Bugs 0 Features 2
Metric Value
wmc 56
c 6
b 0
f 2
lcom 1
cbo 2
dl 0
loc 451
rs 6.5957

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 3
A buildIndex() 0 14 3
A assertIndexIsBuild() 0 5 2
A getPropertyIds() 0 10 1
A getByPropertyId() 0 9 2
A getFlatArrayIndexOfObject() 0 12 3
A toFlatArray() 0 9 2
A getFlatArrayIndices() 0 17 3
B moveObjectInPropertyGroup() 0 30 5
A moveObjectToEndOfPropertyGroup() 0 15 2
A removeObject() 0 6 1
A insertObjectAtIndex() 0 11 1
C movePropertyGroup() 0 47 9
A getPropertyGroupIndex() 0 13 3
C moveObjectToIndex() 0 27 7
B addObjectAtIndex() 0 25 4
B addObjectToPropertyGroup() 0 37 5

How to fix   Complexity   

Complex Class

Complex classes like ByPropertyIdArray 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 ByPropertyIdArray, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Wikibase\DataModel;
4
5
use ArrayObject;
6
use InvalidArgumentException;
7
use OutOfBoundsException;
8
use RuntimeException;
9
use Traversable;
10
use Wikibase\DataModel\Entity\PropertyId;
11
12
/**
13
 * Helper for managing objects indexed by property id.
14
 *
15
 * This is a light weight alternative approach to using something
16
 * like GenericArrayObject with the advantages that no extra interface
17
 * is needed and that indexing does not happen automatically.
18
 *
19
 * Lack of automatic indexing means that you will need to call the
20
 * buildIndex method before doing any look-ups.
21
 *
22
 * Since no extra interface is used, the user is responsible for only
23
 * adding objects that have a getPropertyId method that returns either
24
 * a string or integer when called with no arguments.
25
 *
26
 * Objects may be added or moved within the structure. Absolute indices (indices according to the
27
 * flat list of objects) may be specified to add or move objects. These management operations take
28
 * the property grouping into account. Adding or moving objects outside their "property groups"
29
 * shifts the whole group towards that index.
30
 *
31
 * Example of moving an object within its "property group":
32
 * o1 (p1)                           o1 (p1)
33
 * o2 (p2)                       /-> o3 (p2)
34
 * o3 (p2) ---> move to index 1 -/   o2 (p2)
35
 *
36
 * Example of moving an object that triggers moving the whole "property group":
37
 * o1 (p1)                       /-> o3 (p2)
38
 * o2 (p2)                       |   o2 (p2)
39
 * o3 (p2) ---> move to index 0 -/   o1 (p1)
40
 *
41
 * @since 0.2
42
 * @deprecated since 5.0, use a DataModel Service instead
43
 *
44
 * @license GPL-2.0+
45
 * @author H. Snater < [email protected] >
46
 */
47
class ByPropertyIdArray extends ArrayObject {
48
49
	/**
50
	 * @var array[]|null
51
	 */
52
	private $byId = null;
53
54
	/**
55
	 * @deprecated since 5.0, use a DataModel Service instead
56
	 * @see ArrayObject::__construct
57
	 *
58
	 * @param PropertyIdProvider[]|Traversable|null $input
59
	 *
60
	 * @throws InvalidArgumentException
61
	 */
62
	public function __construct( $input = null ) {
63
		if ( is_object( $input ) && !( $input instanceof Traversable ) ) {
64
			throw new InvalidArgumentException( '$input must be an array, Traversable or null' );
65
		}
66
67
		parent::__construct( (array)$input );
68
	}
69
70
	/**
71
	 * Builds the index for doing look-ups by property id.
72
	 *
73
	 * @since 0.2
74
	 */
75
	public function buildIndex() {
76
		$this->byId = array();
77
78
		/** @var PropertyIdProvider $object */
79
		foreach ( $this as $object ) {
80
			$propertyId = $object->getPropertyId()->getSerialization();
81
82
			if ( !array_key_exists( $propertyId, $this->byId ) ) {
83
				$this->byId[$propertyId] = array();
84
			}
85
86
			$this->byId[$propertyId][] = $object;
87
		}
88
	}
89
90
	/**
91
	 * Checks whether id indexed array has been generated.
92
	 *
93
	 * @throws RuntimeException
94
	 */
95
	private function assertIndexIsBuild() {
96
		if ( $this->byId === null ) {
97
			throw new RuntimeException( 'Index not build, call buildIndex first' );
98
		}
99
	}
100
101
	/**
102
	 * Returns the property ids used for indexing.
103
	 *
104
	 * @since 0.2
105
	 *
106
	 * @return PropertyId[]
107
	 * @throws RuntimeException
108
	 */
109
	public function getPropertyIds() {
110
		$this->assertIndexIsBuild();
111
112
		return array_map(
113
			function( $serializedPropertyId ) {
114
				return new PropertyId( $serializedPropertyId );
115
			},
116
			array_keys( $this->byId )
117
		);
118
	}
119
120
	/**
121
	 * Returns the objects featuring the provided property id in the index.
122
	 *
123
	 * @since 0.2
124
	 *
125
	 * @param PropertyId $propertyId
126
	 *
127
	 * @throws OutOfBoundsException
128
	 * @throws RuntimeException
129
	 * @return PropertyIdProvider[]
130
	 */
131
	public function getByPropertyId( PropertyId $propertyId ) {
132
		$this->assertIndexIsBuild();
133
134
		if ( !( array_key_exists( $propertyId->getSerialization(), $this->byId ) ) ) {
135
			throw new OutOfBoundsException( "Object with propertyId \"$propertyId\" not found" );
136
		}
137
138
		return $this->byId[$propertyId->getSerialization()];
139
	}
140
141
	/**
142
	 * Returns the absolute index of an object or false if the object could not be found.
143
	 * @since 0.5
144
	 *
145
	 * @param PropertyIdProvider $object
146
	 *
147
	 * @return bool|int
148
	 * @throws RuntimeException
149
	 */
150
	public function getFlatArrayIndexOfObject( $object ) {
151
		$this->assertIndexIsBuild();
152
153
		$i = 0;
154
		foreach ( $this as $o ) {
155
			if ( $o === $object ) {
156
				return $i;
157
			}
158
			$i++;
159
		}
160
		return false;
161
	}
162
163
	/**
164
	 * Returns the objects in a flat array (using the indexed form for generating the array).
165
	 * @since 0.5
166
	 *
167
	 * @return PropertyIdProvider[]
168
	 * @throws RuntimeException
169
	 */
170
	public function toFlatArray() {
171
		$this->assertIndexIsBuild();
172
173
		$array = array();
174
		foreach ( $this->byId as $objects ) {
0 ignored issues
show
Bug introduced by
The expression $this->byId of type array<integer,array>|null 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...
175
			$array = array_merge( $array, $objects );
176
		}
177
		return $array;
178
	}
179
180
	/**
181
	 * Returns the absolute numeric indices of objects featuring the same property id.
182
	 *
183
	 * @param PropertyId $propertyId
184
	 *
185
	 * @throws RuntimeException
186
	 * @return int[]
187
	 */
188
	private function getFlatArrayIndices( PropertyId $propertyId ) {
189
		$this->assertIndexIsBuild();
190
191
		$propertyIndices = array();
192
		$i = 0;
193
194
		foreach ( $this->byId as $serializedPropertyId => $objects ) {
0 ignored issues
show
Bug introduced by
The expression $this->byId of type array<integer,array>|null 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...
195
			if ( $serializedPropertyId === $propertyId->getSerialization() ) {
196
				$propertyIndices = range( $i, $i + count( $objects ) - 1 );
197
				break;
198
			} else {
199
				$i += count( $objects );
200
			}
201
		}
202
203
		return $propertyIndices;
204
	}
205
206
	/**
207
	 * Moves an object within its "property group".
208
	 *
209
	 * @param PropertyIdProvider $object
210
	 * @param int $toIndex Absolute index within a "property group".
211
	 *
212
	 * @throws OutOfBoundsException
213
	 */
214
	private function moveObjectInPropertyGroup( $object, $toIndex ) {
215
		$currentIndex = $this->getFlatArrayIndexOfObject( $object );
216
217
		if ( $toIndex === $currentIndex ) {
218
			return;
219
		}
220
221
		$propertyId = $object->getPropertyId();
222
223
		$numericIndices = $this->getFlatArrayIndices( $propertyId );
224
		$lastIndex = $numericIndices[count( $numericIndices ) - 1];
225
226
		if ( $toIndex > $lastIndex + 1 || $toIndex < $numericIndices[0] ) {
227
			throw new OutOfBoundsException( 'Object cannot be moved to ' . $toIndex );
228
		}
229
230
		if ( $toIndex >= $lastIndex ) {
231
			$this->moveObjectToEndOfPropertyGroup( $object );
232
		} else {
233
			$this->removeObject( $object );
234
235
			$propertyGroup = array_combine(
236
				$this->getFlatArrayIndices( $propertyId ),
237
				$this->getByPropertyId( $propertyId )
238
			);
239
240
			$insertBefore = $propertyGroup[$toIndex];
241
			$this->insertObjectAtIndex( $object, $this->getFlatArrayIndexOfObject( $insertBefore ) );
0 ignored issues
show
Security Bug introduced by
It seems like $this->getFlatArrayIndexOfObject($insertBefore) targeting Wikibase\DataModel\ByPro...latArrayIndexOfObject() can also be of type false; however, Wikibase\DataModel\ByPro...::insertObjectAtIndex() does only seem to accept integer, did you maybe forget to handle an error condition?
Loading history...
242
		}
243
	}
244
245
	/**
246
	 * Moves an object to the end of its "property group".
247
	 *
248
	 * @param PropertyIdProvider $object
249
	 */
250
	private function moveObjectToEndOfPropertyGroup( $object ) {
251
		$this->removeObject( $object );
252
253
		$propertyId = $object->getPropertyId();
254
		$propertyIdSerialization = $propertyId->getSerialization();
255
256
		$propertyGroup = in_array( $propertyIdSerialization, $this->getPropertyIds() )
257
			? $this->getByPropertyId( $propertyId )
258
			: array();
259
260
		$propertyGroup[] = $object;
261
		$this->byId[$propertyIdSerialization] = $propertyGroup;
262
263
		$this->exchangeArray( $this->toFlatArray() );
264
	}
265
266
	/**
267
	 * Removes an object from the array structures.
268
	 *
269
	 * @param PropertyIdProvider $object
270
	 */
271
	private function removeObject( $object ) {
272
		$flatArray = $this->toFlatArray();
273
		$this->exchangeArray( $flatArray );
274
		$this->offsetUnset( array_search( $object, $flatArray ) );
275
		$this->buildIndex();
276
	}
277
278
	/**
279
	 * Inserts an object at a specific index.
280
	 *
281
	 * @param PropertyIdProvider $object
282
	 * @param int $index Absolute index within the flat list of objects.
283
	 */
284
	private function insertObjectAtIndex( $object, $index ) {
285
		$flatArray = $this->toFlatArray();
286
287
		$this->exchangeArray( array_merge(
288
			array_slice( $flatArray, 0, $index ),
289
			array( $object ),
290
			array_slice( $flatArray, $index )
291
		) );
292
293
		$this->buildIndex();
294
	}
295
296
	/**
297
	 * @param PropertyId $propertyId
298
	 * @param int $toIndex
299
	 */
300
	private function movePropertyGroup( PropertyId $propertyId, $toIndex ) {
301
		if ( $this->getPropertyGroupIndex( $propertyId ) === $toIndex ) {
302
			return;
303
		}
304
305
		$insertBefore = null;
306
307
		$oldIndex = $this->getPropertyGroupIndex( $propertyId );
308
		$byIdClone = $this->byId;
309
310
		// Remove "property group" to calculate the groups new index:
311
		unset( $this->byId[$propertyId->getSerialization()] );
312
313
		if ( $toIndex > $oldIndex ) {
314
			// If the group shall be moved towards the bottom, the number of objects within the
315
			// group needs to be subtracted from the absolute toIndex:
316
			$toIndex -= count( $byIdClone[$propertyId->getSerialization()] );
317
		}
318
319
		foreach ( $this->getPropertyIds() as $pId ) {
320
			// Accepting other than the exact index by using <= letting the "property group" "latch"
321
			// in the next slot.
322
			if ( $toIndex <= $this->getPropertyGroupIndex( $pId ) ) {
323
				$insertBefore = $pId;
324
				break;
325
			}
326
		}
327
328
		$serializedPropertyId = $propertyId->getSerialization();
329
		$this->byId = array();
330
331
		foreach ( $byIdClone as $serializedPId => $objects ) {
0 ignored issues
show
Bug introduced by
The expression $byIdClone of type array<integer,array>|null 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...
332
			$pId = new PropertyId( $serializedPId );
333
			if ( $pId->equals( $propertyId ) ) {
334
				continue;
335
			} elseif ( $pId->equals( $insertBefore ) ) {
336
				$this->byId[$serializedPropertyId] = $byIdClone[$serializedPropertyId];
337
			}
338
			$this->byId[$serializedPId] = $objects;
339
		}
340
341
		if ( $insertBefore === null ) {
342
			$this->byId[$serializedPropertyId] = $byIdClone[$serializedPropertyId];
343
		}
344
345
		$this->exchangeArray( $this->toFlatArray() );
346
	}
347
348
	/**
349
	 * Returns the index of a "property group" (the first object in the flat array that features
350
	 * the specified property). Returns false if property id could not be found.
351
	 *
352
	 * @param PropertyId $propertyId
353
	 *
354
	 * @return bool|int
355
	 */
356
	private function getPropertyGroupIndex( PropertyId $propertyId ) {
357
		$i = 0;
358
359
		foreach ( $this->byId as $serializedPropertyId => $objects ) {
0 ignored issues
show
Bug introduced by
The expression $this->byId of type array<integer,array>|null 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...
360
			$pId = new PropertyId( $serializedPropertyId );
361
			if ( $pId->equals( $propertyId ) ) {
362
				return $i;
363
			}
364
			$i += count( $objects );
365
		}
366
367
		return false;
368
	}
369
370
	/**
371
	 * Moves an existing object to a new index. Specifying an index outside the object's "property
372
	 * group" will move the object to the edge of the "property group" and shift the whole group
373
	 * to achieve the designated index for the object to move.
374
	 * @since 0.5
375
	 *
376
	 * @param PropertyIdProvider $object
377
	 * @param int $toIndex Absolute index where to move the object to.
378
	 *
379
	 * @throws OutOfBoundsException
380
	 * @throws RuntimeException
381
	 */
382
	public function moveObjectToIndex( $object, $toIndex ) {
383
		$this->assertIndexIsBuild();
384
385
		if ( !in_array( $object, $this->toFlatArray() ) ) {
386
			throw new OutOfBoundsException( 'Object not present in array' );
387
		} elseif ( $toIndex < 0 || $toIndex > count( $this ) ) {
388
			throw new OutOfBoundsException( 'Specified index is out of bounds' );
389
		} elseif ( $this->getFlatArrayIndexOfObject( $object ) === $toIndex ) {
390
			return;
391
		}
392
393
		// Determine whether to simply reindex the object within its "property group":
394
		$propertyIndices = $this->getFlatArrayIndices( $object->getPropertyId() );
395
396
		if ( in_array( $toIndex, $propertyIndices ) ) {
397
			$this->moveObjectInPropertyGroup( $object, $toIndex );
398
		} else {
399
			$edgeIndex = ( $toIndex <= $propertyIndices[0] )
400
				? $propertyIndices[0]
401
				: $propertyIndices[count( $propertyIndices ) - 1];
402
403
			$this->moveObjectInPropertyGroup( $object, $edgeIndex );
404
			$this->movePropertyGroup( $object->getPropertyId(), $toIndex );
405
		}
406
407
		$this->exchangeArray( $this->toFlatArray() );
408
	}
409
410
	/**
411
	 * Adds an object at a specific index. If no index is specified, the object will be append to
412
	 * the end of its "property group" or - if no objects featuring the same property exist - to the
413
	 * absolute end of the array.
414
	 * Specifying an index outside a "property group" will place the new object at the specified
415
	 * index with the existing "property group" objects being shifted towards the new object.
416
	 *
417
	 * @since 0.5
418
	 *
419
	 * @param PropertyIdProvider $object
420
	 * @param int|null $index Absolute index where to place the new object.
421
	 *
422
	 * @throws OutOfBoundsException
423
	 * @throws RuntimeException
424
	 */
425
	public function addObjectAtIndex( $object, $index = null ) {
426
		$this->assertIndexIsBuild();
427
428
		$propertyId = $object->getPropertyId();
429
		$validIndices = $this->getFlatArrayIndices( $propertyId );
430
431
		if ( count( $this ) === 0 ) {
432
			// Array is empty, just append object.
433
			$this->append( $object );
434
		} elseif ( empty( $validIndices ) ) {
435
			// No objects featuring that property exist. The object may be inserted at a place
436
			// between existing "property groups".
437
			$this->append( $object );
438
			if ( $index !== null ) {
439
				$this->buildIndex();
440
				$this->moveObjectToIndex( $object, $index );
441
			}
442
		} else {
443
			// Objects featuring the same property as the object which is about to be added already
444
			// exist in the array.
445
			$this->addObjectToPropertyGroup( $object, $index );
446
		}
447
448
		$this->buildIndex();
449
	}
450
451
	/**
452
	 * Adds an object to an existing property group at the specified absolute index.
453
	 *
454
	 * @param PropertyIdProvider $object
455
	 * @param int|null $index
456
	 *
457
	 * @throws OutOfBoundsException
458
	 */
459
	private function addObjectToPropertyGroup( $object, $index = null ) {
460
		$propertyId = $object->getPropertyId();
461
		$validIndices = $this->getFlatArrayIndices( $propertyId );
462
463
		if ( empty( $validIndices ) ) {
464
			throw new OutOfBoundsException( 'No objects featuring the object\'s property exist' );
465
		}
466
467
		// Add index to allow placing object after the last object of the "property group":
468
		$validIndices[] = $validIndices[count( $validIndices ) - 1] + 1;
469
470
		if ( $index === null ) {
471
			// If index is null, append object to "property group".
472
			$index = $validIndices[count( $validIndices ) - 1];
473
		}
474
475
		if ( in_array( $index, $validIndices ) ) {
476
			// Add object at index within "property group".
477
			$this->byId[$propertyId->getSerialization()][] = $object;
478
			$this->exchangeArray( $this->toFlatArray() );
479
			$this->moveObjectToIndex( $object, $index );
480
481
		} else {
482
			// Index is out of the "property group"; The whole group needs to be moved.
483
			$this->movePropertyGroup( $propertyId, $index );
484
485
			// Move new object to the edge of the "property group" to receive its designated
486
			// index:
487
			if ( $index < $validIndices[0] ) {
488
				array_unshift( $this->byId[$propertyId->getSerialization()], $object );
489
			} else {
490
				$this->byId[$propertyId->getSerialization()][] = $object;
491
			}
492
		}
493
494
		$this->exchangeArray( $this->toFlatArray() );
495
	}
496
497
}
498