Completed
Push — 2.x ( f49b88...9f2b65 )
by Leszek
15:52 queued 15:50
created

Diff::unserialize()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 13
cts 13
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 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() {
81 122
		return $this->getArrayCopy();
82
	}
83
84
	/**
85
	 * @since 0.1
86
	 *
87
	 * @param string $type
88
	 *
89
	 * @return DiffOp[]
90
	 */
91 22
	public function getTypeOperations( $type ) {
92 22
		return array_intersect_key(
93 22
			$this->getArrayCopy(),
94 22
			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 ) {
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 44
	public function unserialize( $serialization ) {
147 44
		$serializationData = unserialize( $serialization );
148
149 44
		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 38
			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 44
		$this->indexOffset = $serializationData['index'];
156
157 44
		$this->typePointers = $serializationData['typePointers'];
158
159 44
		if ( array_key_exists( 'assoc', $serializationData ) ) {
160 43
			$this->isAssociative = $serializationData['assoc'] === 'n' ? null : $serializationData['assoc'] === 't';
161
		} // The below cases are compat with < 0.4.
162 1
		elseif ( $this instanceof MapDiff ) {
163 1
			$this->isAssociative = true;
164
		}
165 1
		elseif ( $this instanceof ListDiff ) {
166 1
			$this->isAssociative = false;
167
		}
168 44
	}
169
170
	/**
171
	 * @since 0.1
172
	 *
173
	 * @return DiffOpAdd[]
174
	 */
175 8
	public function getAdditions() {
176 8
		return $this->getTypeOperations( 'add' );
177
	}
178
179
	/**
180
	 * @since 0.1
181
	 *
182
	 * @return DiffOpRemove[]
183
	 */
184 8
	public function getRemovals() {
185 8
		return $this->getTypeOperations( 'remove' );
186
	}
187
188
	/**
189
	 * @since 0.1
190
	 *
191
	 * @return DiffOpChange[]
192
	 */
193 1
	public function getChanges() {
194 1
		return $this->getTypeOperations( 'change' );
195
	}
196
197
	/**
198
	 * Returns the added values.
199
	 *
200
	 * @since 0.1
201
	 *
202
	 * @return array of mixed
203
	 */
204 1
	public function getAddedValues() {
205 1
		return array_map(
206
			function( DiffOpAdd $addition ) {
207 1
				return $addition->getNewValue();
208 1
			},
209 1
			$this->getTypeOperations( 'add' )
210
		);
211
	}
212
213
	/**
214
	 * Returns the removed values.
215
	 *
216
	 * @since 0.1
217
	 *
218
	 * @return array of mixed
219
	 */
220 1
	public function getRemovedValues() {
221 1
		return array_map(
222 1
			function( DiffOpRemove $addition ) {
223 1
				return $addition->getOldValue();
224 1
			},
225 1
			$this->getTypeOperations( 'remove' )
226
		);
227
	}
228
229
	/**
230
	 * @see DiffOp::isAtomic
231
	 *
232
	 * @since 0.1
233
	 *
234
	 * @return bool
235
	 */
236 72
	public function isAtomic() {
237 72
		return false;
238
	}
239
240
	/**
241
	 * @see DiffOp::getType
242
	 *
243
	 * @since 0.1
244
	 *
245
	 * @return string
246
	 */
247 152
	public function getType() {
248 152
		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
	 * holds two atomic operations will be 3.
256
	 *
257
	 * @see Countable::count
258
	 *
259
	 * @since 0.1
260
	 *
261
	 * @return int
262
	 */
263 85
	public function count() {
264 85
		$count = 0;
265
266
		/**
267
		 * @var DiffOp $diffOp
268
		 */
269 85
		foreach ( $this as $diffOp ) {
270 69
			$count += $diffOp->count();
271
		}
272
273 85
		return $count;
274
	}
275
276
	/**
277
	 * @since 0.3
278
	 */
279 1
	public function removeEmptyOperations() {
280 1
		foreach ( $this->getArrayCopy() as $key => $operation ) {
281 1
			if ( $operation instanceof self && $operation->isEmpty() ) {
282 1
				unset( $this[$key] );
283
			}
284
		}
285 1
	}
286
287
	/**
288
	 * Returns the value of the isAssociative flag.
289
	 *
290
	 * @since 0.4
291
	 *
292
	 * @return bool|null
293
	 */
294 10
	public function isAssociative() {
295 10
		return $this->isAssociative;
296
	}
297
298
	/**
299
	 * Returns if the diff looks associative or not.
300
	 * 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 9
	public function looksAssociative() {
308 9
		return $this->isAssociative === null ? $this->hasAssociativeOperations() : $this->isAssociative;
309
	}
310
311
	/**
312
	 * Returns if the diff can be non-associative.
313
	 * This means it does not contain any non-add-non-remove operations.
314
	 *
315
	 * @since 0.4
316
	 *
317
	 * @return bool
318
	 */
319 16
	public function hasAssociativeOperations() {
320 16
		return !empty( $this->typePointers['change'] )
321 14
			|| !empty( $this->typePointers['diff'] )
322 12
			|| !empty( $this->typePointers['map'] )
323 16
			|| !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
	 *
331
	 * @since 0.5
332
	 *
333
	 * @param callable|null $valueConverter optional callback used to convert any
334
	 *        complex values to arrays.
335
	 *
336
	 * @return array
337
	 */
338 108
	public function toArray( $valueConverter = null ) {
339 108
		$operations = array();
340
341 108
		foreach ( $this->getOperations() as $key => $diffOp ) {
342 96
			$operations[$key] = $diffOp->toArray( $valueConverter );
343
		}
344
345
		return array(
346 108
			'type' => $this->getType(),
347 108
			'isassoc' => $this->isAssociative,
348 108
			'operations' => $operations
349
		);
350
	}
351
352
	/**
353
	 * @since 2.0
354
	 *
355
	 * @param mixed $target
356
	 *
357
	 * @return bool
358
	 */
359 14
	public function equals( $target ) {
360 14
		if ( $target === $this ) {
361 1
			return true;
362
		}
363
364 13
		if ( !( $target instanceof self ) ) {
365 2
			return false;
366
		}
367
368 11
		return $this->isAssociative === $target->isAssociative
369 11
			&& $this->getArrayCopy() == $target->getArrayCopy();
370
	}
371
372
	/**
373
	 * 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
	 *
377
	 * @return int
378
	 */
379 23
	private function getNewOffset() {
380 23
		while ( $this->offsetExists( $this->indexOffset ) ) {
381 13
			$this->indexOffset++;
382
		}
383
384 23
		return $this->indexOffset;
385
	}
386
387
	/**
388
	 * @see ArrayObject::append
389
	 *
390
	 * @since 0.1
391
	 *
392
	 * @param mixed $value
393
	 */
394 19
	public function append( $value ) {
395 19
		$this->setElement( null, $value );
396 10
	}
397
398
	/**
399
	 * @see ArrayObject::offsetSet()
400
	 *
401
	 * @since 0.1
402
	 *
403
	 * @param int|string $index
404
	 * @param mixed $value
405
	 */
406 72
	public function offsetSet( $index, $value ) {
407 72
		$this->setElement( $index, $value );
408 71
	}
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
	 * otherwise needs to be executed whenever an element is added,
417
	 * you can overload @see preSetElement.
418
	 *
419
	 * @param int|string|null $index
420
	 * @param mixed $value
421
	 *
422
	 * @throws InvalidArgumentException
423
	 */
424 86
	private function setElement( $index, $value ) {
425 86
		if ( !( $value instanceof DiffOp ) ) {
426 12
			throw new InvalidArgumentException(
427 12
				'Can only add DiffOp implementing objects to ' . get_called_class() . '.'
428
			);
429
		}
430
431 84
		if ( $index === null ) {
432 23
			$index = $this->getNewOffset();
433
		}
434
435 84
		if ( $this->preSetElement( $index, $value ) ) {
436 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...
437
		}
438 76
	}
439
440
	/**
441
	 * @see Serializable::serialize
442
	 *
443
	 * @since 0.1
444
	 *
445
	 * @return string
446
	 */
447 43
	public function serialize() {
448 43
		$assoc = $this->isAssociative === null ? 'n' : ( $this->isAssociative ? 't' : 'f' );
449
450
		$data = array(
451 43
			'data' => $this->getArrayCopy(),
452 43
			'index' => $this->indexOffset,
453 43
			'typePointers' => $this->typePointers,
454 43
			'assoc' => $assoc
455
		);
456
457 43
		return serialize( $data );
458
	}
459
460
	/**
461
	 * Returns if the ArrayObject has no elements.
462
	 *
463
	 * @since 0.1
464
	 *
465
	 * @return bool
466
	 */
467 15
	public function isEmpty() {
468 15
		return $this->count() === 0;
469
	}
470
471
}
472