Completed
Pull Request — master (#824)
by
unknown
17:59
created

SnakList::getSnak()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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