Passed
Push — revert-interface-change ( ccb51e...f9a91e )
by Jakob
06:03 queued 01:30
created

HashArray::__construct()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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