Passed
Push — equalHashArrays ( ea52b8 )
by no
02:53
created

HashArray::hasValidType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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