Completed
Push — master ( e62317...cd1faf )
by adam
03:51 queued 01:43
created

SnakList::__serialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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