Completed
Pull Request — master (#713)
by Bekh-Ivanov
33:57 queued 30:36
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 2.5

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 1
cts 2
cp 0.5
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
crap 2.5
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 8
	 *
101 8
	 * @since 0.1
102 8
	 *
103 8
	 * @param int|string $index
104
	 * @param Hashable $hashable
105 8
	 *
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 8
	 * Returns if there is an element with the provided hash.
125 8
	 *
126
	 * @since 0.1
127 8
	 *
128
	 * @param string $elementHash
129 8
	 *
130
	 * @return bool
131
	 */
132
	public function hasElementHash( $elementHash ) {
133 8
		return array_key_exists( $elementHash, $this->offsetHashes );
134 4
	}
135 2
136 2
	/**
137
	 * Returns if there is an element with the same hash as the provided element in the list.
138 4
	 *
139 4
	 * @since 0.1
140
	 *
141 4
	 * @param Hashable $element
142
	 *
143
	 * @return bool
144 8
	 */
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 21
		$this->removeByElementHash( $element->getHash() );
158 21
	}
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 12
			$this->offsetUnset( $offset );
171 12
		}
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 8
	 * @return bool Indicates if the element was added or not.
182 8
	 */
183 8
	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 9
193 9
	/**
194 9
	 * Returns the element with the provided hash or false if there is no such element.
195
	 *
196 9
	 * @since 0.1
197 3
	 *
198 3
	 * @param string $elementHash
199
	 *
200 9
	 * @return mixed|bool
201 9
	 */
202 9
	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 12
	 * @see ArrayObject::offsetUnset
214 12
	 *
215
	 * @since 0.1
216 12
	 *
217 8
	 * @param mixed $index
218 8
	 */
219
	public function offsetUnset( $index ) {
220 12
		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 13
		$this->setElement( null, $value );
255 13
	}
256
257
	/**
258
	 * @see ArrayObject::offsetSet()
259 13
	 *
260
	 * @param mixed $index
261 13
	 * @param mixed $value
262
	 */
263 13
	public function offsetSet( $index, $value ) {
264 13
		$this->setElement( $index, $value );
265 13
	}
266 3
267 3
	/**
268 3
	 * Returns if the provided value has the same type as the elements
269 3
	 * that can be added to this ArrayObject.
270
	 *
271 3
	 * @param mixed $value
272 3
	 *
273
	 * @return bool
274 12
	 */
275
	protected function hasValidType( $value ) {
276
		$class = $this->getObjectType();
277 13
		return $value instanceof $class;
278 13
	}
279 13
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 4
	 * @param mixed $value
291 4
	 *
292 4
	 * @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 4
			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 4
		}
308 4
	}
309
310
	/**
311
	 * @see Serializable::serialize
312 4
	 *
313
	 * @return string
314
	 */
315
	public function serialize() {
316
		return serialize( [
317
			'data' => $this->getArrayCopy(),
318
			'index' => $this->indexOffset,
319
		] );
320 12
	}
321 12
322
	/**
323
	 * @see Serializable::unserialize
324
	 *
325
	 * @param string $serialized
326 12
	 */
327 12
	public function unserialize( $serialized ) {
328
		$serializationData = unserialize( $serialized );
329 12
330 3
		foreach ( $serializationData['data'] as $offset => $value ) {
331 3
			// Just set the element, bypassing checks and offset resolving,
332
			// as these elements have already gone through this.
333 12
			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 12
336 12
		$this->indexOffset = $serializationData['index'];
337
	}
338
339
	/**
340
	 * @return bool
341
	 */
342
	public function isEmpty() {
343
		return !$this->getIterator()->valid();
344
	}
345
346
}
347