Completed
Push — 21x ( 0e3ed4...f4f4da )
by adam
17:20 queued 21s
created

Diff::unserialize()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

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