Passed
Push — int32EntityId ( fa80fb )
by no
05:13
created

HashArray::getObjectType()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 1
nc 1
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 = array();
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( array(), $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] = array( $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 = array();
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
	 * Returns if the hash indices are up to date.
339
	 * For an HashArray with immutable objects this should always be the case.
340
	 * For one with mutable objects it's the responsibility of the mutating code
341
	 * to keep the indices up to date (see class documentation) and thus possible
342
	 * this has not been done since the last update, thus causing a state where
343
	 * one or more indices are out of date.
344
	 *
345
	 * @since 0.4
346
	 *
347
	 * @return bool
348
	 */
349
	public function indicesAreUpToDate() {
350
		foreach ( $this->offsetHashes as $hash => $offsets ) {
351
			$offsets = (array)$offsets;
352
353
			foreach ( $offsets as $offset ) {
354
				/** @var Hashable[] $this */
355
				if ( $this[$offset]->getHash() !== $hash ) {
356
					return false;
357
				}
358
			}
359
		}
360
361
		return true;
362
	}
363
364
	/**
365
	 * Removes and adds all elements, ensuring the indices are up to date.
366
	 *
367
	 * @since 0.4
368
	 */
369
	public function rebuildIndices() {
370
		$hashables = iterator_to_array( $this );
371
372
		$this->offsetHashes = array();
373
374
		foreach ( $hashables as $offset => $hashable ) {
375
			$this->offsetUnset( $offset );
376
			$this->offsetSet( $offset, $hashable );
377
		}
378
	}
379
380
	/**
381
	 * @see ArrayObject::append
382
	 *
383
	 * @param mixed $value
384
	 */
385
	public function append( $value ) {
386
		$this->setElement( null, $value );
387
	}
388
389
	/**
390
	 * @see ArrayObject::offsetSet()
391
	 *
392
	 * @param mixed $index
393
	 * @param mixed $value
394
	 */
395
	public function offsetSet( $index, $value ) {
396
		$this->setElement( $index, $value );
397
	}
398
399
	/**
400
	 * Returns if the provided value has the same type as the elements
401
	 * that can be added to this ArrayObject.
402
	 *
403
	 * @param mixed $value
404
	 *
405
	 * @return bool
406
	 */
407
	protected function hasValidType( $value ) {
408
		$class = $this->getObjectType();
409
		return $value instanceof $class;
410
	}
411
412
	/**
413
	 * Method that actually sets the element and holds
414
	 * all common code needed for set operations, including
415
	 * type checking and offset resolving.
416
	 *
417
	 * If you want to do additional indexing or have code that
418
	 * otherwise needs to be executed whenever an element is added,
419
	 * you can overload @see preSetElement.
420
	 *
421
	 * @param mixed $index
422
	 * @param mixed $value
423
	 *
424
	 * @throws InvalidArgumentException
425
	 */
426
	protected function setElement( $index, $value ) {
427
		if ( !$this->hasValidType( $value ) ) {
428
			$type = is_object( $value ) ? get_class( $value ) : gettype( $value );
429
430
			throw new InvalidArgumentException( '$value must be an instance of ' . $this->getObjectType() . '; got ' . $type );
431
		}
432
433
		if ( $index === null ) {
434
			$index = $this->getNewOffset();
435
		}
436
437
		if ( $this->preSetElement( $index, $value ) ) {
438
			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...
439
		}
440
	}
441
442
	/**
443
	 * @see Serializable::serialize
444
	 *
445
	 * @return string
446
	 */
447
	public function serialize() {
448
		return serialize( array(
449
			'data' => $this->getArrayCopy(),
450
			'index' => $this->indexOffset,
451
		) );
452
	}
453
454
	/**
455
	 * @see Serializable::unserialize
456
	 *
457
	 * @param string $serialized
458
	 */
459
	public function unserialize( $serialized ) {
460
		$serializationData = unserialize( $serialized );
461
462
		foreach ( $serializationData['data'] as $offset => $value ) {
463
			// Just set the element, bypassing checks and offset resolving,
464
			// as these elements have already gone through this.
465
			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...
466
		}
467
468
		$this->indexOffset = $serializationData['index'];
469
	}
470
471
	/**
472
	 * Returns if the ArrayObject has no elements.
473
	 *
474
	 * @return bool
475
	 */
476
	public function isEmpty() {
477
		return !$this->getIterator()->valid();
478
	}
479
480
}
481