Passed
Push — master ( 989ec2...f800f0 )
by Leszek
44s
created

HashArray::offsetSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
crap 2
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 8
	 * @since 0.1
101 8
	 *
102 8
	 * @param int|string $index
103 8
	 * @param Hashable $hashable
104
	 *
105 8
	 * @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
		}
115
		else {
116
			$this->offsetHashes[$hash] = $index;
117
118
			return true;
119
		}
120
	}
121
122
	/**
123
	 * Returns if there is an element with the provided hash.
124 8
	 *
125 8
	 * @since 0.1
126
	 *
127 8
	 * @param string $elementHash
128
	 *
129 8
	 * @return bool
130
	 */
131
	public function hasElementHash( $elementHash ) {
132
		return array_key_exists( $elementHash, $this->offsetHashes );
133 8
	}
134 4
135 2
	/**
136 2
	 * Returns if there is an element with the same hash as the provided element in the list.
137
	 *
138 4
	 * @since 0.1
139 4
	 *
140
	 * @param Hashable $element
141 4
	 *
142
	 * @return bool
143
	 */
144 8
	public function hasElement( Hashable $element ) {
145
		return $this->hasElementHash( $element->getHash() );
146
	}
147
148
	/**
149
	 * Removes the element with the hash of the provided element, if there is such an element in the list.
150
	 *
151
	 * @since 0.1
152
	 *
153
	 * @param Hashable $element
154
	 */
155
	public function removeElement( Hashable $element ) {
156
		$this->removeByElementHash( $element->getHash() );
157 21
	}
158 21
159
	/**
160
	 * Removes the element with the provided hash, if there is such an element in the list.
161
	 *
162
	 * @since 0.1
163
	 *
164
	 * @param string $elementHash
165
	 */
166
	public function removeByElementHash( $elementHash ) {
167
		if ( $this->hasElementHash( $elementHash ) ) {
168
			$offset = $this->offsetHashes[$elementHash];
169
			$this->offsetUnset( $offset );
170 12
		}
171 12
	}
172
173
	/**
174
	 * Adds the provided element to the list if there is no element with the same hash yet.
175
	 *
176
	 * @since 0.1
177
	 *
178
	 * @param Hashable $element
179
	 *
180
	 * @return bool Indicates if the element was added or not.
181 8
	 */
182 8
	public function addElement( Hashable $element ) {
183 8
		$append = !$this->hasElementHash( $element->getHash() );
184
185
		if ( $append ) {
186
			$this->append( $element );
187
		}
188
189
		return $append;
190
	}
191
192 9
	/**
193 9
	 * Returns the element with the provided hash or false if there is no such element.
194 9
	 *
195
	 * @since 0.1
196 9
	 *
197 3
	 * @param string $elementHash
198 3
	 *
199
	 * @return mixed|bool
200 9
	 */
201 9
	public function getByElementHash( $elementHash ) {
202 9
		if ( $this->hasElementHash( $elementHash ) ) {
203
			$offset = $this->offsetHashes[$elementHash];
204
			return $this->offsetGet( $offset );
205
		}
206
		else {
207
			return false;
208
		}
209
	}
210
211
	/**
212
	 * @see ArrayObject::offsetUnset
213 12
	 *
214 12
	 * @since 0.1
215
	 *
216 12
	 * @param mixed $index
217 8
	 */
218 8
	public function offsetUnset( $index ) {
219
		if ( $this->offsetExists( $index ) ) {
220 12
			/**
221
			 * @var Hashable $element
222
			 */
223
			$element = $this->offsetGet( $index );
224
225
			$hash = $element->getHash();
226
227
			unset( $this->offsetHashes[$hash] );
228
229
			parent::offsetUnset( $index );
230
		}
231
	}
232
233
	/**
234
	 * @see ArrayObject::append
235
	 *
236
	 * @param mixed $value
237
	 */
238
	public function append( $value ) {
239
		$this->setElement( null, $value );
240
	}
241
242
	/**
243
	 * @see ArrayObject::offsetSet()
244
	 *
245
	 * @param mixed $index
246
	 * @param mixed $value
247
	 */
248
	public function offsetSet( $index, $value ) {
249
		$this->setElement( $index, $value );
250
	}
251
252
	/**
253
	 * Returns if the provided value has the same type as the elements
254 13
	 * that can be added to this ArrayObject.
255 13
	 *
256
	 * @param mixed $value
257
	 *
258
	 * @return bool
259 13
	 */
260
	protected function hasValidType( $value ) {
261 13
		$class = $this->getObjectType();
262
		return $value instanceof $class;
263 13
	}
264 13
265 13
	/**
266 3
	 * Method that actually sets the element and holds
267 3
	 * all common code needed for set operations, including
268 3
	 * type checking and offset resolving.
269 3
	 *
270
	 * If you want to do additional indexing or have code that
271 3
	 * otherwise needs to be executed whenever an element is added,
272 3
	 * you can overload @see preSetElement.
273
	 *
274 12
	 * @param mixed $index
275
	 * @param mixed $value
276
	 *
277 13
	 * @throws InvalidArgumentException
278 13
	 */
279 13
	protected function setElement( $index, $value ) {
280
		if ( !$this->hasValidType( $value ) ) {
281
			$type = is_object( $value ) ? get_class( $value ) : gettype( $value );
282
283
			throw new InvalidArgumentException( '$value must be an instance of ' . $this->getObjectType() . '; got ' . $type );
284
		}
285
286
		if ( $index === null ) {
287
			$index = $this->getNewOffset();
288
		}
289
290 4
		if ( $this->preSetElement( $index, $value ) ) {
291 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...
292 4
		}
293
	}
294
295
	/**
296
	 * @see Serializable::serialize
297
	 *
298
	 * @return string
299
	 */
300
	public function serialize() {
301
		return serialize( [
302
			'data' => $this->getArrayCopy(),
303
			'index' => $this->indexOffset,
304
		] );
305
	}
306 4
307 4
	/**
308 4
	 * @see Serializable::unserialize
309
	 *
310
	 * @param string $serialized
311
	 */
312 4
	public function unserialize( $serialized ) {
313
		$serializationData = unserialize( $serialized );
314
315
		foreach ( $serializationData['data'] as $offset => $value ) {
316
			// Just set the element, bypassing checks and offset resolving,
317
			// as these elements have already gone through this.
318
			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...
319
		}
320 12
321 12
		$this->indexOffset = $serializationData['index'];
322
	}
323
324
	/**
325
	 * @return bool
326 12
	 */
327 12
	public function isEmpty() {
328
		return !$this->getIterator()->valid();
329 12
	}
330 3
331
}
332