Passed
Push — php70 ( c1737a...b053c6 )
by Jeroen De
05:56 queued 02:23
created

Diff::unserialize()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 8
nc 6
nop 1
crap 4
1
<?php
2
3
namespace Diff\DiffOp\Diff;
4
5
use ArrayObject;
6
use Diff\DiffOp\DiffOp;
7
use Diff\DiffOp\DiffOpAdd;
8
use Diff\DiffOp\DiffOpChange;
9
use Diff\DiffOp\DiffOpRemove;
10
use InvalidArgumentException;
11
12
/**
13
 * Base class for diffs. Diffs are collections of DiffOp objects,
14
 * and are themselves DiffOp objects as well.
15
 *
16
 * @since 0.1
17
 *
18
 * @license GPL-2.0+
19
 * @author Jeroen De Dauw < [email protected] >
20
 * @author Daniel Kinzler
21
 * @author Thiemo Mättig
22
 */
23
class Diff extends ArrayObject implements DiffOp {
24
25
	/**
26
	 * @var bool|null
27
	 */
28
	private $isAssociative = null;
29
30
	/**
31
	 * Pointers to the operations of certain types for quick lookup.
32
	 *
33
	 * @var array[]
34
	 */
35
	private $typePointers = array(
36
		'add' => array(),
37
		'remove' => array(),
38
		'change' => array(),
39
		'list' => array(),
40
		'map' => array(),
41
		'diff' => array(),
42
	);
43
44
	/**
45
	 * @var int
46
	 */
47
	private $indexOffset = 0;
48
49
	/**
50
	 * @since 0.1
51
	 *
52
	 * @param DiffOp[] $operations
53
	 * @param bool|null $isAssociative
54
	 *
55
	 * @throws InvalidArgumentException
56
	 */
57 105
	public function __construct( array $operations = array(), $isAssociative = null ) {
58 105
		if ( $isAssociative !== null && !is_bool( $isAssociative ) ) {
59 6
			throw new InvalidArgumentException( '$isAssociative should be a boolean or null' );
60
		}
61
62 99
		parent::__construct( array() );
63
64 99
		foreach ( $operations as $offset => $operation ) {
65 64
			if ( !( $operation instanceof DiffOp ) ) {
66 4
				throw new InvalidArgumentException( 'All elements fed to the Diff constructor should be of type DiffOp' );
67
			}
68
69 61
			$this->offsetSet( $offset, $operation );
70
		}
71
72 95
		$this->isAssociative = $isAssociative;
73 95
	}
74
75
	/**
76
	 * @since 0.1
77
	 *
78
	 * @return DiffOp[]
79
	 */
80 122
	public function getOperations(): array {
81 122
		return $this->getArrayCopy();
82
	}
83
84
	/**
85
	 * @since 0.1
86
	 *
87
	 * @param string $type
88
	 *
89
	 * @return DiffOp[]
90
	 */
91 21
	public function getTypeOperations( string $type ): array {
92 21
		return array_intersect_key(
93 21
			$this->getArrayCopy(),
94 21
			array_flip( $this->typePointers[$type] )
95
		);
96
	}
97
98
	/**
99
	 * @since 0.1
100
	 *
101
	 * @param DiffOp[] $operations
102
	 */
103 7
	public function addOperations( array $operations ) {
104 7
		foreach ( $operations as $operation ) {
105 5
			$this->append( $operation );
106
		}
107 7
	}
108
109
	/**
110
	 * Gets called before a new element is added to the ArrayObject.
111
	 *
112
	 * At this point the index is always set (ie not null) and the
113
	 * value is always of the type returned by @see getObjectType.
114
	 *
115
	 * Should return a boolean. When false is returned the element
116
	 * does not get added to the ArrayObject.
117
	 *
118
	 * @param int|string $index
119
	 * @param DiffOp $value
120
	 *
121
	 * @return bool
122
	 * @throws InvalidArgumentException
123
	 */
124 84
	private function preSetElement( $index, DiffOp $value ): bool {
125 84
		if ( $this->isAssociative === false && ( $value->getType() !== 'add' && $value->getType() !== 'remove' ) ) {
126 1
			throw new InvalidArgumentException( 'Diff operation with invalid type "' . $value->getType() . '" provided.' );
127
		}
128
129 83
		if ( array_key_exists( $value->getType(), $this->typePointers ) ) {
130 76
			$this->typePointers[$value->getType()][] = $index;
131
		}
132
		else {
133 7
			throw new InvalidArgumentException( 'Diff operation with invalid type "' . $value->getType() . '" provided.' );
134
		}
135
136 76
		return true;
137
	}
138
139
	/**
140
	 * @see Serializable::unserialize
141
	 *
142
	 * @since 0.1
143
	 *
144
	 * @param string $serialization
145
	 */
146 43
	public function unserialize( $serialization ) {
147 43
		$serializationData = unserialize( $serialization );
148
149 43
		foreach ( $serializationData['data'] as $offset => $value ) {
150
			// Just set the element, bypassing checks and offset resolving,
151
			// as these elements have already gone through this.
152 37
			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...
153
		}
154
155 43
		$this->indexOffset = $serializationData['index'];
156
157 43
		$this->typePointers = $serializationData['typePointers'];
158
159 43
		if ( array_key_exists( 'assoc', $serializationData ) ) {
160 43
			$this->isAssociative = $serializationData['assoc'] === 'n' ? null : $serializationData['assoc'] === 't';
161
		}
162 43
	}
163
164
	/**
165
	 * @since 0.1
166
	 *
167
	 * @return DiffOpAdd[]
168
	 */
169 7
	public function getAdditions(): array {
170 7
		return $this->getTypeOperations( 'add' );
171
	}
172
173
	/**
174
	 * @since 0.1
175
	 *
176
	 * @return DiffOpRemove[]
177
	 */
178 7
	public function getRemovals(): array {
179 7
		return $this->getTypeOperations( 'remove' );
180
	}
181
182
	/**
183
	 * @since 0.1
184
	 *
185
	 * @return DiffOpChange[]
186
	 */
187
	public function getChanges(): array {
188
		return $this->getTypeOperations( 'change' );
189
	}
190
191
	/**
192
	 * Returns the added values.
193
	 *
194
	 * @since 0.1
195
	 *
196
	 * @return array of mixed
197
	 */
198 1
	public function getAddedValues(): array {
199 1
		return array_map(
200
			function( DiffOpAdd $addition ) {
201 1
				return $addition->getNewValue();
202 1
			},
203 1
			$this->getTypeOperations( 'add' )
204
		);
205
	}
206
207
	/**
208
	 * Returns the removed values.
209
	 *
210
	 * @since 0.1
211
	 *
212
	 * @return array of mixed
213
	 */
214 1
	public function getRemovedValues(): array {
215 1
		return array_map(
216 1
			function( DiffOpRemove $addition ) {
217 1
				return $addition->getOldValue();
218 1
			},
219 1
			$this->getTypeOperations( 'remove' )
220
		);
221
	}
222
223
	/**
224
	 * @see DiffOp::isAtomic
225
	 *
226
	 * @since 0.1
227
	 *
228
	 * @return bool
229
	 */
230 72
	public function isAtomic(): bool {
231 72
		return false;
232
	}
233
234
	/**
235
	 * @see DiffOp::getType
236
	 *
237
	 * @since 0.1
238
	 *
239
	 * @return string
240
	 */
241 152
	public function getType(): string {
242 152
		return 'diff';
243
	}
244
245
	/**
246
	 * Counts the number of atomic operations in the diff.
247
	 * This means the size of a diff with as elements only empty diffs will be 0.
248
	 * Or that the size of a diff with one atomic operation and one diff that itself
249
	 * holds two atomic operations will be 3.
250
	 *
251
	 * @see Countable::count
252
	 *
253
	 * @since 0.1
254
	 *
255
	 * @return int
256
	 */
257 84
	public function count(): int {
258 84
		$count = 0;
259
260
		/**
261
		 * @var DiffOp $diffOp
262
		 */
263 84
		foreach ( $this as $diffOp ) {
264 68
			$count += $diffOp->count();
265
		}
266
267 84
		return $count;
268
	}
269
270
	/**
271
	 * @since 0.3
272
	 */
273 1
	public function removeEmptyOperations() {
274 1
		foreach ( $this->getArrayCopy() as $key => $operation ) {
275 1
			if ( $operation instanceof self && $operation->isEmpty() ) {
276 1
				unset( $this[$key] );
277
			}
278
		}
279 1
	}
280
281
	/**
282
	 * Returns the value of the isAssociative flag.
283
	 *
284
	 * @since 0.4
285
	 *
286
	 * @return bool|null
287
	 */
288 9
	public function isAssociative() {
289 9
		return $this->isAssociative;
290
	}
291
292
	/**
293
	 * Returns if the diff looks associative or not.
294
	 * This first checks the isAssociative flag and in case its null checks
295
	 * if there are any non-add-non-remove operations.
296
	 *
297
	 * @since 0.4
298
	 *
299
	 * @return bool
300
	 */
301 9
	public function looksAssociative(): bool {
302 9
		return $this->isAssociative === null ? $this->hasAssociativeOperations() : $this->isAssociative;
303
	}
304
305
	/**
306
	 * Returns if the diff can be non-associative.
307
	 * This means it does not contain any non-add-non-remove operations.
308
	 *
309
	 * @since 0.4
310
	 *
311
	 * @return bool
312
	 */
313 16
	public function hasAssociativeOperations(): bool {
314 16
		return !empty( $this->typePointers['change'] )
315 14
			|| !empty( $this->typePointers['diff'] )
316 12
			|| !empty( $this->typePointers['map'] )
317 16
			|| !empty( $this->typePointers['list'] );
318
	}
319
320
	/**
321
	 * Returns the Diff in array form where nested DiffOps are also turned into their array form.
322
	 *
323
	 * @see  DiffOp::toArray
324
	 *
325
	 * @since 0.5
326
	 *
327
	 * @param callable|null $valueConverter optional callback used to convert any
328
	 *        complex values to arrays.
329
	 *
330
	 * @return array
331
	 */
332 108
	public function toArray( callable $valueConverter = null ): array {
333 108
		$operations = array();
334
335 108
		foreach ( $this->getOperations() as $key => $diffOp ) {
336 96
			$operations[$key] = $diffOp->toArray( $valueConverter );
337
		}
338
339
		return array(
340 108
			'type' => $this->getType(),
341 108
			'isassoc' => $this->isAssociative,
342 108
			'operations' => $operations
343
		);
344
	}
345
346
	/**
347
	 * @since 2.0
348
	 *
349
	 * @param mixed $target
350
	 *
351
	 * @return bool
352
	 */
353 12
	public function equals( $target ) {
354 12
		if ( $target === $this ) {
355 1
			return true;
356
		}
357
358 11
		if ( !( $target instanceof self ) ) {
359 2
			return false;
360
		}
361
362 9
		return $this->isAssociative === $target->isAssociative
363 9
			&& $this->getArrayCopy() == $target->getArrayCopy();
364
	}
365
366
	/**
367
	 * Finds a new offset for when appending an element.
368
	 * The base class does this, so it would be better to integrate,
369
	 * but there does not appear to be any way to do this...
370
	 *
371
	 * @return int
372
	 */
373 23
	private function getNewOffset(): int {
374 23
		while ( $this->offsetExists( $this->indexOffset ) ) {
375 13
			$this->indexOffset++;
376
		}
377
378 23
		return $this->indexOffset;
379
	}
380
381
	/**
382
	 * @see ArrayObject::append
383
	 *
384
	 * @since 0.1
385
	 *
386
	 * @param mixed $value
387
	 */
388 19
	public function append( $value ) {
389 19
		$this->setElement( null, $value );
390 10
	}
391
392
	/**
393
	 * @see ArrayObject::offsetSet()
394
	 *
395
	 * @since 0.1
396
	 *
397
	 * @param int|string $index
398
	 * @param mixed $value
399
	 */
400 72
	public function offsetSet( $index, $value ) {
401 72
		$this->setElement( $index, $value );
402 71
	}
403
404
	/**
405
	 * Method that actually sets the element and holds
406
	 * all common code needed for set operations, including
407
	 * type checking and offset resolving.
408
	 *
409
	 * If you want to do additional indexing or have code that
410
	 * otherwise needs to be executed whenever an element is added,
411
	 * you can overload @see preSetElement.
412
	 *
413
	 * @param int|string|null $index
414
	 * @param mixed $value
415
	 *
416
	 * @throws InvalidArgumentException
417
	 */
418 86
	private function setElement( $index, $value ) {
419 86
		if ( !( $value instanceof DiffOp ) ) {
420 12
			throw new InvalidArgumentException(
421 12
				'Can only add DiffOp implementing objects to ' . get_called_class() . '.'
422
			);
423
		}
424
425 84
		if ( $index === null ) {
426 23
			$index = $this->getNewOffset();
427
		}
428
429 84
		if ( $this->preSetElement( $index, $value ) ) {
430 76
			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...
431
		}
432 76
	}
433
434
	/**
435
	 * @see Serializable::serialize
436
	 *
437
	 * @since 0.1
438
	 *
439
	 * @return string
440
	 */
441 43
	public function serialize() {
442 43
		$assoc = $this->isAssociative === null ? 'n' : ( $this->isAssociative ? 't' : 'f' );
443
444
		$data = array(
445 43
			'data' => $this->getArrayCopy(),
446 43
			'index' => $this->indexOffset,
447 43
			'typePointers' => $this->typePointers,
448 43
			'assoc' => $assoc
449
		);
450
451 43
		return serialize( $data );
452
	}
453
454
	/**
455
	 * Returns if the ArrayObject has no elements.
456
	 *
457
	 * @since 0.1
458
	 *
459
	 * @return bool
460
	 */
461 15
	public function isEmpty(): bool {
462 15
		return $this->count() === 0;
463
	}
464
465
}
466