Completed
Push — master ( c4a39f...bc6260 )
by Jeroen De
40s
created

HashArray::offsetUnset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 3.1852

Importance

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