Passed
Push — master ( e3acee...71d19a )
by Jeroen De
02:21
created

src/DiffOp/Diff/Diff.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 BSD-3-Clause
21
 * @author Jeroen De Dauw < [email protected] >
22
 * @author Daniel Kinzler
23
 * @author Thiemo Kreuz
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 = [
38
		'add' => [],
39
		'remove' => [],
40
		'change' => [],
41
		'list' => [],
42
		'map' => [],
43
		'diff' => [],
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 = [], $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( [] );
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
	 * @since 0.1
195
	 *
196
	 * @return array of mixed
197
	 */
198 1
	public function getAddedValues(): array {
199 1
		return array_map(
200 1
			function( DiffOpAdd $addition ) {
201 1
				return $addition->getNewValue();
202 1
			},
203 1
			$this->getTypeOperations( 'add' )
204
		);
205
	}
206
207
	/**
208
	 * @since 0.1
209
	 *
210
	 * @return array of mixed
211
	 */
212 1
	public function getRemovedValues(): array {
213 1
		return array_map(
214 1
			function( DiffOpRemove $addition ) {
215 1
				return $addition->getOldValue();
216 1
			},
217 1
			$this->getTypeOperations( 'remove' )
218
		);
219
	}
220
221
	/**
222
	 * @see DiffOp::isAtomic
223
	 *
224
	 * @since 0.1
225
	 *
226
	 * @return bool
227
	 */
228 72
	public function isAtomic(): bool {
229 72
		return false;
230
	}
231
232
	/**
233
	 * @see DiffOp::getType
234
	 *
235
	 * @since 0.1
236
	 *
237
	 * @return string
238
	 */
239 152
	public function getType(): string {
240 152
		return 'diff';
241
	}
242
243
	/**
244
	 * Counts the number of atomic operations in the diff.
245
	 * This means the size of a diff with as elements only empty diffs will be 0.
246
	 * Or that the size of a diff with one atomic operation and one diff that itself
247
	 * holds two atomic operations will be 3.
248
	 *
249
	 * @see Countable::count
250
	 *
251
	 * @since 0.1
252
	 *
253
	 * @return int
254
	 */
255 74
	public function count(): int {
256 74
		$count = 0;
257
258
		/**
259
		 * @var DiffOp $diffOp
260
		 */
261 74
		foreach ( $this as $diffOp ) {
262 62
			$count += $diffOp->count();
263
		}
264
265 74
		return $count;
266
	}
267
268
	/**
269
	 * @since 0.3
270
	 */
271 1
	public function removeEmptyOperations() {
272 1
		foreach ( $this->getArrayCopy() as $key => $operation ) {
273 1
			if ( $operation instanceof self && $operation->isEmpty() ) {
274 1
				unset( $this[$key] );
275
			}
276
		}
277 1
	}
278
279
	/**
280
	 * Returns the value of the isAssociative flag.
281
	 *
282
	 * @since 0.4
283
	 *
284
	 * @return bool|null
285
	 */
286 9
	public function isAssociative() {
287 9
		return $this->isAssociative;
288
	}
289
290
	/**
291
	 * Returns if the diff looks associative or not.
292
	 * This first checks the isAssociative flag and in case its null checks
293
	 * if there are any non-add-non-remove operations.
294
	 *
295
	 * @since 0.4
296
	 *
297
	 * @return bool
298
	 */
299 9
	public function looksAssociative(): bool {
300 9
		return $this->isAssociative === null ? $this->hasAssociativeOperations() : $this->isAssociative;
301
	}
302
303
	/**
304
	 * Returns if the diff can be non-associative.
305
	 * This means it does not contain any non-add-non-remove operations.
306
	 *
307
	 * @since 0.4
308
	 *
309
	 * @return bool
310
	 */
311 16
	public function hasAssociativeOperations(): bool {
312 16
		return !empty( $this->typePointers['change'] )
313 14
			|| !empty( $this->typePointers['diff'] )
314 12
			|| !empty( $this->typePointers['map'] )
315 16
			|| !empty( $this->typePointers['list'] );
316
	}
317
318
	/**
319
	 * Returns the Diff in array form where nested DiffOps are also turned into their array form.
320
	 *
321
	 * @see  DiffOp::toArray
322
	 *
323
	 * @since 0.5
324
	 *
325
	 * @param callable|null $valueConverter optional callback used to convert any
326
	 *        complex values to arrays.
327
	 *
328
	 * @return array
329
	 */
330 108
	public function toArray( callable $valueConverter = null ): array {
331 108
		$operations = [];
332
333 108
		foreach ( $this->getOperations() as $key => $diffOp ) {
334 96
			$operations[$key] = $diffOp->toArray( $valueConverter );
335
		}
336
337
		return [
338 108
			'type' => $this->getType(),
339 108
			'isassoc' => $this->isAssociative,
340 108
			'operations' => $operations
341
		];
342
	}
343
344
	/**
345
	 * @since 2.0
346
	 *
347
	 * @param mixed $target
348
	 *
349
	 * @return bool
350
	 */
351 12
	public function equals( $target ) {
352 12
		if ( $target === $this ) {
353 1
			return true;
354
		}
355
356 11
		if ( !( $target instanceof self ) ) {
357 2
			return false;
358
		}
359
360 9
		return $this->isAssociative === $target->isAssociative
361 9
			&& $this->getArrayCopy() == $target->getArrayCopy();
362
	}
363
364
	/**
365
	 * Finds a new offset for when appending an element.
366
	 * The base class does this, so it would be better to integrate,
367
	 * but there does not appear to be any way to do this...
368
	 *
369
	 * @return int
370
	 */
371 23
	private function getNewOffset(): int {
372 23
		while ( $this->offsetExists( $this->indexOffset ) ) {
373 13
			$this->indexOffset++;
374
		}
375
376 23
		return $this->indexOffset;
377
	}
378
379
	/**
380
	 * @see ArrayObject::append
381
	 *
382
	 * @since 0.1
383
	 *
384
	 * @param mixed $value
385
	 */
386 19
	public function append( $value ) {
387 19
		$this->setElement( null, $value );
388 10
	}
389
390
	/**
391
	 * @see ArrayObject::offsetSet()
392
	 *
393
	 * @since 0.1
394
	 *
395
	 * @param int|string $index
396
	 * @param mixed $value
397
	 */
398 72
	public function offsetSet( $index, $value ) {
399 72
		$this->setElement( $index, $value );
400 71
	}
401
402
	/**
403
	 * Method that actually sets the element and holds
404
	 * all common code needed for set operations, including
405
	 * type checking and offset resolving.
406
	 *
407
	 * If you want to do additional indexing or have code that
408
	 * otherwise needs to be executed whenever an element is added,
409
	 * you can overload @see preSetElement.
410
	 *
411
	 * @param int|string|null $index
412
	 * @param mixed $value
413
	 *
414
	 * @throws InvalidArgumentException
415
	 */
416 86
	private function setElement( $index, $value ) {
417 86
		if ( !( $value instanceof DiffOp ) ) {
418 12
			throw new InvalidArgumentException(
419 12
				'Can only add DiffOp implementing objects to ' . get_called_class() . '.'
420
			);
421
		}
422
423 84
		if ( $index === null ) {
424 23
			$index = $this->getNewOffset();
425
		}
426
427 84
		if ( $this->preSetElement( $index, $value ) ) {
428 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...
429
		}
430 76
	}
431
432
	/**
433
	 * @see Serializable::serialize
434
	 *
435
	 * @since 0.1
436
	 *
437
	 * @return string
438
	 */
439 43
	public function serialize() {
440 43
		$assoc = $this->isAssociative === null ? 'n' : ( $this->isAssociative ? 't' : 'f' );
441
442
		$data = [
443 43
			'data' => $this->getArrayCopy(),
444 43
			'index' => $this->indexOffset,
445 43
			'typePointers' => $this->typePointers,
446 43
			'assoc' => $assoc
447
		];
448
449 43
		return serialize( $data );
450
	}
451
452
	/**
453
	 * @since 0.1
454
	 *
455
	 * @return bool
456
	 */
457 15
	public function isEmpty(): bool {
458
		/** @var DiffOp $diffOp */
459 15
		foreach ( $this as $diffOp ) {
460 11
			if ( $diffOp->count() > 0 ) {
461 11
				return false;
462
			}
463
		}
464
465 7
		return true;
466
	}
467
468
}
469