Passed
Push — rm-hasharray ( 353511 )
by Jeroen De
03:16
created

SnakList::offsetUnset()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 8.439
c 0
b 0
f 0
cc 5
eloc 14
nc 3
nop 1
1
<?php
2
3
namespace Wikibase\DataModel\Snak;
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
 * List of Snak objects.
14
 * Indexes the snaks by hash and ensures no more the one snak with the same hash are in the list.
15
 *
16
 * @since 0.1
17
 *
18
 * @license GPL-2.0+
19
 * @author Jeroen De Dauw < [email protected] >
20
 * @author Addshore
21
 */
22
class SnakList extends ArrayObject implements Hashable, Comparable {
23
24
	/**
25
	 * Maps snak hashes to their offsets.
26
	 *
27
	 * @var array [ snak hash (string) => array [ snak offset (string|int) ] | snak offset (string|int) ]
28
	 */
29
	private $offsetHashes = [];
30
31
	/**
32
	 * @var integer
33
	 */
34
	private $indexOffset = 0;
35
36
	/**
37
	 * @see ArrayObject::__construct
38
	 *
39
	 * @param array|Traversable|null $input
40
	 * @param int $flags
41
	 * @param string $iteratorClass
42
	 *
43
	 * @throws InvalidArgumentException
44
	 */
45
	public function __construct( $input = null, $flags = 0, $iteratorClass = 'ArrayIterator' ) {
46
		parent::__construct( [], $flags, $iteratorClass );
47
48
		if ( $input !== null ) {
49
			if ( !is_array( $input ) && !( $input instanceof Traversable ) ) {
50
				throw new InvalidArgumentException( '$input must be an array or Traversable' );
51
			}
52
53
			foreach ( $input as $offset => $value ) {
54
				$this->offsetSet( $offset, $value );
55
			}
56
		}
57
	}
58
59
	/**
60
	 * Finds a new offset for when appending an element.
61
	 * The base class does this, so it would be better to integrate,
62
	 * but there does not appear to be any way to do this...
63
	 *
64
	 * @return integer
65
	 */
66
	private function getNewOffset() {
67
		while ( $this->offsetExists( $this->indexOffset ) ) {
68
			$this->indexOffset++;
69
		}
70
71
		return $this->indexOffset;
72
	}
73
74
	/**
75
	 * Gets called before a new element is added to the list.
76
	 *
77
	 * Should return a boolean. When false is returned the element
78
	 * does not get added to the ArrayObject.
79
	 *
80
	 * @since 0.1
81
	 *
82
	 * @param int|string $index
83
	 * @param Hashable $hashable
84
	 *
85
	 * @return bool
86
	 */
87
	private function preSetElement( $index, $hashable ) {
88
		$hash = $hashable->getHash();
89
90
		$hasHash = $this->hasSnakHash( $hash );
91
92
		if ( $hasHash ) {
93
			return false;
94
		}
95
96
		if ( $hasHash ) {
97
			if ( !is_array( $this->offsetHashes[$hash] ) ) {
98
				$this->offsetHashes[$hash] = [ $this->offsetHashes[$hash] ];
99
			}
100
101
			$this->offsetHashes[$hash][] = $index;
102
		}
103
		else {
104
			$this->offsetHashes[$hash] = $index;
105
		}
106
107
		return true;
108
	}
109
110
	/**
111
	 * @see ArrayObject::offsetUnset
112
	 *
113
	 * @since 0.1
114
	 *
115
	 * @param mixed $index
116
	 */
117
	public function offsetUnset( $index ) {
118
		if ( $this->offsetExists( $index ) ) {
119
			/**
120
			 * @var Hashable $element
121
			 */
122
			$element = $this->offsetGet( $index );
123
124
			$hash = $element->getHash();
125
126
			if ( array_key_exists( $hash, $this->offsetHashes )
127
				&& is_array( $this->offsetHashes[$hash] )
128
				&& count( $this->offsetHashes[$hash] ) > 1 ) {
129
				$this->offsetHashes[$hash] = array_filter(
130
					$this->offsetHashes[$hash],
131
					function( $value ) use ( $index ) {
132
						return $value !== $index;
133
					}
134
				);
135
			}
136
			else {
137
				unset( $this->offsetHashes[$hash] );
138
			}
139
140
			parent::offsetUnset( $index );
141
		}
142
	}
143
144
	/**
145
	 * @see Hashable::getHash
146
	 *
147
	 * The hash is purely valuer based. Order of the elements in the array is not held into account.
148
	 *
149
	 * @since 0.1
150
	 *
151
	 * @return string
152
	 */
153
	public function getHash() {
154
		$hasher = new MapValueHasher();
155
		return $hasher->hash( $this );
156
	}
157
158
	/**
159
	 * @see Comparable::equals
160
	 *
161
	 * The comparison is done purely value based, ignoring the order of the elements in the array.
162
	 *
163
	 * @since 0.3
164
	 *
165
	 * @param mixed $target
166
	 *
167
	 * @return bool
168
	 */
169
	public function equals( $target ) {
170
		if ( $this === $target ) {
171
			return true;
172
		}
173
174
		return $target instanceof self
175
			&& $this->getHash() === $target->getHash();
176
	}
177
178
	/**
179
	 * Removes duplicates bases on hash value.
180
	 *
181
	 * @since 0.3
182
	 */
183
	public function removeDuplicates() {
184
		$knownHashes = [];
185
186
		/**
187
		 * @var Hashable $hashable
188
		 */
189
		foreach ( iterator_to_array( $this ) as $hashable ) {
190
			$hash = $hashable->getHash();
191
192
			if ( in_array( $hash, $knownHashes ) ) {
193
				$this->removeSnakHash( $hash );
194
			}
195
			else {
196
				$knownHashes[] = $hash;
197
			}
198
		}
199
	}
200
201
	/**
202
	 * @see ArrayObject::append
203
	 *
204
	 * @param mixed $value
205
	 */
206
	public function append( $value ) {
207
		$this->setElement( null, $value );
208
	}
209
210
	/**
211
	 * @see ArrayObject::offsetSet()
212
	 *
213
	 * @param mixed $index
214
	 * @param mixed $value
215
	 */
216
	public function offsetSet( $index, $value ) {
217
		$this->setElement( $index, $value );
218
	}
219
220
	/**
221
	 * Method that actually sets the element and holds
222
	 * all common code needed for set operations, including
223
	 * type checking and offset resolving.
224
	 *
225
	 * If you want to do additional indexing or have code that
226
	 * otherwise needs to be executed whenever an element is added,
227
	 * you can overload @see preSetElement.
228
	 *
229
	 * @param mixed $index
230
	 * @param mixed $value
231
	 *
232
	 * @throws InvalidArgumentException
233
	 */
234
	private function setElement( $index, $value ) {
235
		if ( ! $value instanceof Snak ) {
236
			$type = is_object( $value ) ? get_class( $value ) : gettype( $value );
237
238
			throw new InvalidArgumentException( '$value must be an instance of Snak; got ' . $type );
239
		}
240
241
		if ( $index === null ) {
242
			$index = $this->getNewOffset();
243
		}
244
245
		if ( $this->preSetElement( $index, $value ) ) {
246
			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...
247
		}
248
	}
249
250
	/**
251
	 * @see Serializable::serialize
252
	 *
253
	 * @return string
254
	 */
255
	public function serialize() {
256
		return serialize( [
257
			'data' => $this->getArrayCopy(),
258
			'index' => $this->indexOffset,
259
		] );
260
	}
261
262
	/**
263
	 * @see Serializable::unserialize
264
	 *
265
	 * @param string $serialized
266
	 */
267
	public function unserialize( $serialized ) {
268
		$serializationData = unserialize( $serialized );
269
270
		foreach ( $serializationData['data'] as $offset => $value ) {
271
			// Just set the element, bypassing checks and offset resolving,
272
			// as these elements have already gone through this.
273
			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...
274
		}
275
276
		$this->indexOffset = $serializationData['index'];
277
	}
278
279
	/**
280
	 * Returns if the ArrayObject has no elements.
281
	 *
282
	 * @return bool
283
	 */
284
	public function isEmpty() {
285
		return !$this->getIterator()->valid();
286
	}
287
288
	/**
289
	 * @since 0.1
290
	 *
291
	 * @param string $snakHash
292
	 *
293
	 * @return boolean
294
	 */
295
	public function hasSnakHash( $snakHash ) {
296
		return array_key_exists( $snakHash, $this->offsetHashes );
297
	}
298
299
	/**
300
	 * @since 0.1
301
	 *
302
	 * @param string $snakHash
303
	 */
304
	public function removeSnakHash( $snakHash ) {
305
		if ( $this->hasSnakHash( $snakHash ) ) {
306
			$offset = $this->offsetHashes[$snakHash];
307
308
			if ( is_array( $offset ) ) {
309
				$offset = reset( $offset );
310
			}
311
312
			$this->offsetUnset( $offset );
313
		}
314
	}
315
316
	/**
317
	 * @since 0.1
318
	 *
319
	 * @param Snak $snak
320
	 *
321
	 * @return boolean Indicates if the snak was added or not.
322
	 */
323
	public function addSnak( Snak $snak ) {
324
		if ( $this->hasSnak( $snak ) ) {
325
			return false;
326
		}
327
328
		$this->append( $snak );
329
		return true;
330
	}
331
332
	/**
333
	 * @since 0.1
334
	 *
335
	 * @param Snak $snak
336
	 *
337
	 * @return boolean
338
	 */
339
	public function hasSnak( Snak $snak ) {
340
		return $this->hasSnakHash( $snak->getHash() );
341
	}
342
343
	/**
344
	 * @since 0.1
345
	 *
346
	 * @param Snak $snak
347
	 */
348
	public function removeSnak( Snak $snak ) {
349
		$this->removeSnakHash( $snak->getHash() );
350
	}
351
352
	/**
353
	 * @since 0.1
354
	 *
355
	 * @param string $snakHash
356
	 *
357
	 * @return Snak|bool
358
	 */
359
	public function getSnak( $snakHash ) {
360
		if ( !$this->hasSnakHash( $snakHash ) ) {
361
			return false;
362
		}
363
364
		$offset = $this->offsetHashes[$snakHash];
365
366
		if ( is_array( $offset ) ) {
367
			$offset = reset( $offset );
368
		}
369
370
		return $this->offsetGet( $offset );
371
	}
372
373
	/**
374
	 * Orders the snaks in the list grouping them by property.
375
	 *
376
	 * @param string[] $order List of serliazed property ids to order by.
377
	 *
378
	 * @since 0.5
379
	 */
380
	public function orderByProperty( array $order = [] ) {
381
		$snaksByProperty = $this->getSnaksByProperty();
382
		$orderedProperties = array_unique( array_merge( $order, array_keys( $snaksByProperty ) ) );
383
384
		foreach ( $orderedProperties as $property ) {
385
			if ( array_key_exists( $property, $snaksByProperty ) ) {
386
				$snaks = $snaksByProperty[$property];
387
				$this->moveSnaksToBottom( $snaks );
388
			}
389
		}
390
	}
391
392
	/**
393
	 * @param Snak[] $snaks to remove and re add
394
	 */
395
	private function moveSnaksToBottom( array $snaks ) {
396
		foreach ( $snaks as $snak ) {
397
			$this->removeSnak( $snak );
398
			$this->addSnak( $snak );
399
		}
400
	}
401
402
	/**
403
	 * Gets the snaks in the current object in an array
404
	 * grouped by property id
405
	 *
406
	 * @return array[]
407
	 */
408
	private function getSnaksByProperty() {
409
		$snaksByProperty = [];
410
411
		foreach ( $this as $snak ) {
412
			/** @var Snak $snak */
413
			$propertyId = $snak->getPropertyId()->getSerialization();
414
			if ( !isset( $snaksByProperty[$propertyId] ) ) {
415
				$snaksByProperty[$propertyId] = [];
416
			}
417
			$snaksByProperty[$propertyId][] = $snak;
418
		}
419
420
		return $snaksByProperty;
421
	}
422
423
}
424