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