SnakList::offsetSet()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

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
nc 1
nop 2
crap 2
1
<?php
2
3
namespace Wikibase\DataModel\Snak;
4
5
use ArrayObject;
6
use InvalidArgumentException;
7
use Traversable;
8
use Wikibase\DataModel\Internal\MapValueHasher;
9
10
/**
11
 * List of Snak objects.
12
 * Indexes the snaks by hash and ensures no more the one snak with the same hash are in the list.
13
 *
14
 * @since 0.1
15
 *
16
 * @license GPL-2.0-or-later
17
 * @author Jeroen De Dauw < [email protected] >
18
 * @author Addshore
19
 */
20
class SnakList extends ArrayObject {
21
22
	/**
23
	 * Maps snak hashes to their offsets.
24
	 *
25
	 * @var array [ snak hash (string) => snak offset (string|int) ]
26
	 */
27 7
	private $offsetHashes = [];
28 7
29
	/**
30
	 * @var int
31
	 */
32
	private $indexOffset = 0;
33
34
	/**
35
	 * @param Snak[]|Traversable $snaks
36
	 *
37
	 * @throws InvalidArgumentException
38
	 */
39
	public function __construct( $snaks = [] ) {
40 3
		if ( !is_array( $snaks ) && !( $snaks instanceof Traversable ) ) {
41 3
			throw new InvalidArgumentException( '$snaks must be an array or an instance of Traversable' );
42
		}
43
44
		foreach ( $snaks as $index => $snak ) {
45
			$this->setElement( $index, $snak );
46
		}
47
	}
48
49
	/**
50
	 * @since 0.1
51 5
	 *
52 5
	 * @param string $snakHash
53 5
	 *
54
	 * @return boolean
55
	 */
56
	public function hasSnakHash( $snakHash ) {
57
		return array_key_exists( $snakHash, $this->offsetHashes );
58
	}
59
60
	/**
61
	 * @since 0.1
62
	 *
63
	 * @param string $snakHash
64 10
	 */
65 10
	public function removeSnakHash( $snakHash ) {
66
		if ( $this->hasSnakHash( $snakHash ) ) {
67
			$offset = $this->offsetHashes[$snakHash];
68
			$this->offsetUnset( $offset );
69
		}
70
	}
71
72
	/**
73
	 * @since 0.1
74
	 *
75
	 * @param Snak $snak
76
	 *
77 11
	 * @return boolean Indicates if the snak was added or not.
78 11
	 */
79
	public function addSnak( Snak $snak ) {
80
		if ( $this->hasSnak( $snak ) ) {
81
			return false;
82
		}
83
84
		$this->append( $snak );
85
		return true;
86
	}
87
88 13
	/**
89 13
	 * @since 0.1
90 13
	 *
91
	 * @param Snak $snak
92
	 *
93
	 * @return boolean
94
	 */
95
	public function hasSnak( Snak $snak ) {
96
		return $this->hasSnakHash( $snak->getHash() );
97
	}
98
99
	/**
100
	 * @since 0.1
101
	 *
102
	 * @param Snak $snak
103
	 */
104
	public function removeSnak( Snak $snak ) {
105
		$this->removeSnakHash( $snak->getHash() );
106
	}
107
108
	/**
109
	 * @since 0.1
110
	 *
111
	 * @param string $snakHash
112 7
	 *
113 7
	 * @return Snak|bool
114 7
	 */
115
	public function getSnak( $snakHash ) {
116
		if ( !$this->hasSnakHash( $snakHash ) ) {
117
			return false;
118
		}
119
120
		$offset = $this->offsetHashes[$snakHash];
121
		return $this->offsetGet( $offset );
122
	}
123
124 7
	/**
125 7
	 *
126 7
	 * The comparison is done purely value based, ignoring the order of the elements in the array.
127
	 *
128 7
	 * @since 0.3
129 6
	 *
130 5
	 * @param mixed $target
131 5
	 *
132 5
	 * @return bool
133 7
	 */
134 7
	public function equals( $target ) {
135
		if ( $this === $target ) {
136
			return true;
137
		}
138
139 5
		return $target instanceof self
140 5
			&& $this->getHash() === $target->getHash();
141 5
	}
142 5
143 5
	/**
144 5
	 * The hash is purely value based. Order of the elements in the array is not held into account.
145
	 *
146
	 * @since 0.1
147
	 *
148
	 * @return string
149
	 */
150
	public function getHash() {
151
		$hasher = new MapValueHasher();
152 7
		return $hasher->hash( $this );
153 7
	}
154
155 7
	/**
156
	 * Groups snaks by property, and optionally orders them.
157 5
	 *
158 5
	 * @param string[] $order List of property ID strings to order by. Snaks with other properties
159 5
	 *  will also be grouped, but put at the end, in the order each property appeared first in the
160 5
	 *  original list.
161 5
	 *
162 7
	 * @since 0.5
163
	 */
164 7
	public function orderByProperty( array $order = [] ) {
165
		$byProperty = array_fill_keys( $order, [] );
166
167
		/** @var Snak $snak */
168
		foreach ( $this as $snak ) {
169
			$byProperty[$snak->getPropertyId()->getSerialization()][] = $snak;
170
		}
171
172
		$ordered = [];
173
		foreach ( $byProperty as $snaks ) {
174
			$ordered = array_merge( $ordered, $snaks );
175
		}
176
177
		$this->exchangeArray( $ordered );
178
179
		$index = 0;
180
		foreach ( $ordered as $snak ) {
181
			$this->offsetHashes[$snak->getHash()] = $index++;
182
		}
183
	}
184
185
	/**
186
	 * Finds a new offset for when appending an element.
187
	 * The base class does this, so it would be better to integrate,
188
	 * but there does not appear to be any way to do this...
189
	 *
190
	 * @return int
191
	 */
192
	private function getNewOffset() {
193
		while ( $this->offsetExists( $this->indexOffset ) ) {
194
			$this->indexOffset++;
195
		}
196
197
		return $this->indexOffset;
198
	}
199
200
	/**
201
	 * @see ArrayObject::offsetUnset
202
	 *
203
	 * @since 0.1
204
	 *
205
	 * @param int|string $index
206
	 */
207
	public function offsetUnset( $index ) {
208
		if ( $this->offsetExists( $index ) ) {
209
			/**
210
			 * @var Snak $element
211
			 */
212
			$element = $this->offsetGet( $index );
213
			$hash = $element->getHash();
214
			unset( $this->offsetHashes[$hash] );
215
216
			parent::offsetUnset( $index );
217
		}
218
	}
219
220
	/**
221
	 * @see ArrayObject::append
222
	 *
223
	 * @param Snak $value
224
	 */
225
	public function append( $value ) {
226
		$this->setElement( null, $value );
227
	}
228
229
	/**
230
	 * @see ArrayObject::offsetSet()
231
	 *
232
	 * @param int|string $index
233
	 * @param Snak $value
234
	 */
235
	public function offsetSet( $index, $value ) {
236
		$this->setElement( $index, $value );
237
	}
238
239
	/**
240
	 * Method that actually sets the element and holds
241
	 * all common code needed for set operations, including
242
	 * type checking and offset resolving.
243
	 *
244
	 * @param int|string $index
245
	 * @param Snak $value
246
	 *
247
	 * @throws InvalidArgumentException
248
	 */
249
	private function setElement( $index, $value ) {
250
		if ( !( $value instanceof Snak ) ) {
251
			throw new InvalidArgumentException( '$value must be a Snak' );
252
		}
253
254
		if ( $this->hasSnak( $value ) ) {
255
			return;
256
		}
257
258
		if ( $index === null ) {
259
			$index = $this->getNewOffset();
260
		}
261
262
		$hash = $value->getHash();
263
		$this->offsetHashes[$hash] = $index;
264
		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...
265
	}
266
267
	/**
268
	 * @see Serializable::serialize
269
	 *
270
	 * @return string
271
	 */
272
	public function serialize() {
273
		return serialize( $this->__serialize() );
274
	}
275
276
	/**
277
	 * @see Serializable::unserialize
278
	 *
279
	 * @param string $serialized
280
	 */
281
	public function unserialize( $serialized ) {
282
		$serializationData = unserialize( $serialized );
283
		$this->__unserialize( $serializationData );
284
	}
285
286
	/**
287
	 * @see https://wiki.php.net/rfc/custom_object_serialization
288
	 *
289
	 * @return array
290
	 */
291
	public function __serialize(): array {
292
		return [
293
			'data' => $this->getArrayCopy(),
294
			'index' => $this->indexOffset,
295
		];
296
	}
297
298
	/**
299
	 * @see https://wiki.php.net/rfc/custom_object_serialization
300
	 *
301
	 * @param array $data
302
	 */
303
	public function __unserialize( $data ) : void {
304
		foreach ( $data['data'] as $offset => $value ) {
305
			// Just set the element, bypassing checks and offset resolving,
306
			// as these elements have already gone through this.
307
			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...
308
		}
309
310
		$this->indexOffset = $data['index'];
311
	}
312
313
	/**
314
	 * Returns if the ArrayObject has no elements.
315
	 *
316
	 * @return bool
317
	 */
318
	public function isEmpty() {
319
		return !$this->getIterator()->valid();
320
	}
321
322
}
323