Completed
Push — master ( 1c5793...a5a83d )
by Jeroen De
08:38 queued 05:51
created

Diff::__construct()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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