Passed
Push — indicesAreUpToDate ( 2c2f48...3e88cf )
by no
16:15 queued 13:08
created

HashArray   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 403
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 48
lcom 1
cbo 2
dl 0
loc 403
rs 8.4864
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
getObjectType() 0 1 ?
B __construct() 0 13 5
A getNewOffset() 0 7 2
B preSetElement() 0 23 5
A hasElementHash() 0 3 1
A hasElement() 0 3 1
A removeElement() 0 3 1
A removeByElementHash() 0 11 3
A addElement() 0 9 3
A getByElementHash() 0 14 3
B offsetUnset() 0 26 5
A getHash() 0 4 1
A equals() 0 8 3
A removeDuplicates() 0 17 3
A append() 0 3 1
A offsetSet() 0 3 1
A hasValidType() 0 4 1
B setElement() 0 15 5
A serialize() 0 6 1
A unserialize() 0 11 2
A isEmpty() 0 3 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Wikibase\DataModel;
4
5
use ArrayObject;
6
use Comparable;
7
use Hashable;
8
use InvalidArgumentException;
9
use Traversable;
10
use Wikibase\DataModel\Internal\MapValueHasher;
11
12
/**
13
 * Generic array object with lookups based on hashes of the elements.
14
 *
15
 * Elements need to implement Hashable.
16
 *
17
 * Note that by default the getHash method uses @see MapValueHashesr
18
 * which returns a hash based on the contents of the list, regardless
19
 * of order and keys.
20
 *
21
 * Also note that if the Hashable elements are mutable, any modifications
22
 * made to them via their mutator methods will not cause an update of
23
 * their associated hash in this array.
24
 *
25
 * When acceptDuplicates is set to true, multiple elements with the same
26
 * hash can reside in the HashArray. Lookup by such a non-unique hash will
27
 * return only the first element and deletion will also delete only
28
 * the first such element.
29
 *
30
 * @since 0.1
31
 *
32
 * @license GPL-2.0+
33
 * @author Jeroen De Dauw < [email protected] >
34
 */
35
abstract class HashArray extends ArrayObject implements Hashable, Comparable {
36
37
	/**
38
	 * Maps element hashes to their offsets.
39
	 *
40
	 * @since 0.1
41
	 *
42
	 * @var array [ element hash (string) => array [ element offset (string|int) ] | element offset (string|int) ]
43
	 */
44
	protected $offsetHashes = [];
45
46
	/**
47
	 * If duplicate values (based on hash) should be accepted or not.
48
	 *
49
	 * @since 0.3
50
	 *
51
	 * @var bool
52
	 */
53
	protected $acceptDuplicates = false;
54
55
	/**
56
	 * @var integer
57
	 */
58
	protected $indexOffset = 0;
59
60
	/**
61
	 * Returns the name of an interface/class that the element should implement/extend.
62
	 *
63
	 * @since 0.4
64
	 *
65
	 * @return string
66
	 */
67
	abstract public function getObjectType();
68
69
	/**
70
	 * @see ArrayObject::__construct
71
	 *
72
	 * @param array|Traversable|null $input
73
	 * @param int $flags
74
	 * @param string $iteratorClass
75
	 *
76
	 * @throws InvalidArgumentException
77
	 */
78
	public function __construct( $input = null, $flags = 0, $iteratorClass = 'ArrayIterator' ) {
79
		parent::__construct( [], $flags, $iteratorClass );
80
81
		if ( $input !== null ) {
82
			if ( !is_array( $input ) && !( $input instanceof Traversable ) ) {
83
				throw new InvalidArgumentException( '$input must be an array or Traversable' );
84
			}
85
86
			foreach ( $input as $offset => $value ) {
87
				$this->offsetSet( $offset, $value );
88
			}
89
		}
90
	}
91
92
	/**
93
	 * Finds a new offset for when appending an element.
94
	 * The base class does this, so it would be better to integrate,
95
	 * but there does not appear to be any way to do this...
96
	 *
97
	 * @return integer
98
	 */
99
	protected function getNewOffset() {
100
		while ( $this->offsetExists( $this->indexOffset ) ) {
101
			$this->indexOffset++;
102
		}
103
104
		return $this->indexOffset;
105
	}
106
107
	/**
108
	 * Gets called before a new element is added to the ArrayObject.
109
	 *
110
	 * At this point the index is always set (ie not null) and the
111
	 * value is always of the type returned by @see getObjectType.
112
	 *
113
	 * Should return a boolean. When false is returned the element
114
	 * does not get added to the ArrayObject.
115
	 *
116
	 * @since 0.1
117
	 *
118
	 * @param int|string $index
119
	 * @param Hashable $hashable
120
	 *
121
	 * @return bool
122
	 */
123
	protected function preSetElement( $index, $hashable ) {
124
		$hash = $hashable->getHash();
125
126
		$hasHash = $this->hasElementHash( $hash );
127
128
		if ( !$this->acceptDuplicates && $hasHash ) {
129
			return false;
130
		}
131
		else {
132
			if ( $hasHash ) {
133
				if ( !is_array( $this->offsetHashes[$hash] ) ) {
134
					$this->offsetHashes[$hash] = [ $this->offsetHashes[$hash] ];
135
				}
136
137
				$this->offsetHashes[$hash][] = $index;
138
			}
139
			else {
140
				$this->offsetHashes[$hash] = $index;
141
			}
142
143
			return true;
144
		}
145
	}
146
147
	/**
148
	 * Returns if there is an element with the provided hash.
149
	 *
150
	 * @since 0.1
151
	 *
152
	 * @param string $elementHash
153
	 *
154
	 * @return bool
155
	 */
156
	public function hasElementHash( $elementHash ) {
157
		return array_key_exists( $elementHash, $this->offsetHashes );
158
	}
159
160
	/**
161
	 * Returns if there is an element with the same hash as the provided element in the list.
162
	 *
163
	 * @since 0.1
164
	 *
165
	 * @param Hashable $element
166
	 *
167
	 * @return bool
168
	 */
169
	public function hasElement( Hashable $element ) {
170
		return $this->hasElementHash( $element->getHash() );
171
	}
172
173
	/**
174
	 * Removes the element with the hash of the provided element, if there is such an element in the list.
175
	 *
176
	 * @since 0.1
177
	 *
178
	 * @param Hashable $element
179
	 */
180
	public function removeElement( Hashable $element ) {
181
		$this->removeByElementHash( $element->getHash() );
182
	}
183
184
	/**
185
	 * Removes the element with the provided hash, if there is such an element in the list.
186
	 *
187
	 * @since 0.1
188
	 *
189
	 * @param string $elementHash
190
	 */
191
	public function removeByElementHash( $elementHash ) {
192
		if ( $this->hasElementHash( $elementHash ) ) {
193
			$offset = $this->offsetHashes[$elementHash];
194
195
			if ( is_array( $offset ) ) {
196
				$offset = reset( $offset );
197
			}
198
199
			$this->offsetUnset( $offset );
200
		}
201
	}
202
203
	/**
204
	 * Adds the provided element to the list if there is no element with the same hash yet.
205
	 *
206
	 * @since 0.1
207
	 *
208
	 * @param Hashable $element
209
	 *
210
	 * @return bool Indicates if the element was added or not.
211
	 */
212
	public function addElement( Hashable $element ) {
213
		$append = $this->acceptDuplicates || !$this->hasElementHash( $element->getHash() );
214
215
		if ( $append ) {
216
			$this->append( $element );
217
		}
218
219
		return $append;
220
	}
221
222
	/**
223
	 * Returns the element with the provided hash or false if there is no such element.
224
	 *
225
	 * @since 0.1
226
	 *
227
	 * @param string $elementHash
228
	 *
229
	 * @return mixed|bool
230
	 */
231
	public function getByElementHash( $elementHash ) {
232
		if ( $this->hasElementHash( $elementHash ) ) {
233
			$offset = $this->offsetHashes[$elementHash];
234
235
			if ( is_array( $offset ) ) {
236
				$offset = reset( $offset );
237
			}
238
239
			return $this->offsetGet( $offset );
240
		}
241
		else {
242
			return false;
243
		}
244
	}
245
246
	/**
247
	 * @see ArrayObject::offsetUnset
248
	 *
249
	 * @since 0.1
250
	 *
251
	 * @param mixed $index
252
	 */
253
	public function offsetUnset( $index ) {
254
		if ( $this->offsetExists( $index ) ) {
255
			/**
256
			 * @var Hashable $element
257
			 */
258
			$element = $this->offsetGet( $index );
259
260
			$hash = $element->getHash();
261
262
			if ( array_key_exists( $hash, $this->offsetHashes )
263
				&& is_array( $this->offsetHashes[$hash] )
264
				&& count( $this->offsetHashes[$hash] ) > 1 ) {
265
				$this->offsetHashes[$hash] = array_filter(
266
					$this->offsetHashes[$hash],
267
					function( $value ) use ( $index ) {
268
						return $value !== $index;
269
					}
270
				);
271
			}
272
			else {
273
				unset( $this->offsetHashes[$hash] );
274
			}
275
276
			parent::offsetUnset( $index );
277
		}
278
	}
279
280
	/**
281
	 * @see Hashable::getHash
282
	 *
283
	 * The hash is purely valuer based. Order of the elements in the array is not held into account.
284
	 *
285
	 * @since 0.1
286
	 *
287
	 * @return string
288
	 */
289
	public function getHash() {
290
		$hasher = new MapValueHasher();
291
		return $hasher->hash( $this );
292
	}
293
294
	/**
295
	 * @see Comparable::equals
296
	 *
297
	 * The comparison is done purely value based, ignoring the order of the elements in the array.
298
	 *
299
	 * @since 0.3
300
	 *
301
	 * @param mixed $target
302
	 *
303
	 * @return bool
304
	 */
305
	public function equals( $target ) {
306
		if ( $this === $target ) {
307
			return true;
308
		}
309
310
		return $target instanceof self
311
			&& $this->getHash() === $target->getHash();
312
	}
313
314
	/**
315
	 * Removes duplicates bases on hash value.
316
	 *
317
	 * @since 0.3
318
	 */
319
	public function removeDuplicates() {
320
		$knownHashes = [];
321
322
		/**
323
		 * @var Hashable $hashable
324
		 */
325
		foreach ( iterator_to_array( $this ) as $hashable ) {
326
			$hash = $hashable->getHash();
327
328
			if ( in_array( $hash, $knownHashes ) ) {
329
				$this->removeByElementHash( $hash );
330
			}
331
			else {
332
				$knownHashes[] = $hash;
333
			}
334
		}
335
	}
336
337
	/**
338
	 * @see ArrayObject::append
339
	 *
340
	 * @param mixed $value
341
	 */
342
	public function append( $value ) {
343
		$this->setElement( null, $value );
344
	}
345
346
	/**
347
	 * @see ArrayObject::offsetSet()
348
	 *
349
	 * @param mixed $index
350
	 * @param mixed $value
351
	 */
352
	public function offsetSet( $index, $value ) {
353
		$this->setElement( $index, $value );
354
	}
355
356
	/**
357
	 * Returns if the provided value has the same type as the elements
358
	 * that can be added to this ArrayObject.
359
	 *
360
	 * @param mixed $value
361
	 *
362
	 * @return bool
363
	 */
364
	protected function hasValidType( $value ) {
365
		$class = $this->getObjectType();
366
		return $value instanceof $class;
367
	}
368
369
	/**
370
	 * Method that actually sets the element and holds
371
	 * all common code needed for set operations, including
372
	 * type checking and offset resolving.
373
	 *
374
	 * If you want to do additional indexing or have code that
375
	 * otherwise needs to be executed whenever an element is added,
376
	 * you can overload @see preSetElement.
377
	 *
378
	 * @param mixed $index
379
	 * @param mixed $value
380
	 *
381
	 * @throws InvalidArgumentException
382
	 */
383
	protected function setElement( $index, $value ) {
384
		if ( !$this->hasValidType( $value ) ) {
385
			$type = is_object( $value ) ? get_class( $value ) : gettype( $value );
386
387
			throw new InvalidArgumentException( '$value must be an instance of ' . $this->getObjectType() . '; got ' . $type );
388
		}
389
390
		if ( $index === null ) {
391
			$index = $this->getNewOffset();
392
		}
393
394
		if ( $this->preSetElement( $index, $value ) ) {
395
			parent::offsetSet( $index, $value );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (offsetSet() instead of setElement()). Are you sure this is correct? If so, you might want to change this to $this->offsetSet().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
396
		}
397
	}
398
399
	/**
400
	 * @see Serializable::serialize
401
	 *
402
	 * @return string
403
	 */
404
	public function serialize() {
405
		return serialize( [
406
			'data' => $this->getArrayCopy(),
407
			'index' => $this->indexOffset,
408
		] );
409
	}
410
411
	/**
412
	 * @see Serializable::unserialize
413
	 *
414
	 * @param string $serialized
415
	 */
416
	public function unserialize( $serialized ) {
417
		$serializationData = unserialize( $serialized );
418
419
		foreach ( $serializationData['data'] as $offset => $value ) {
420
			// Just set the element, bypassing checks and offset resolving,
421
			// as these elements have already gone through this.
422
			parent::offsetSet( $offset, $value );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (offsetSet() instead of unserialize()). Are you sure this is correct? If so, you might want to change this to $this->offsetSet().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
423
		}
424
425
		$this->indexOffset = $serializationData['index'];
426
	}
427
428
	/**
429
	 * Returns if the ArrayObject has no elements.
430
	 *
431
	 * @return bool
432
	 */
433
	public function isEmpty() {
434
		return !$this->getIterator()->valid();
435
	}
436
437
}
438