Completed
Pull Request — master (#101)
by
unknown
02:49
created

MapDiffer::createRecursive()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 0
cts 4
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
crap 2
1
<?php
2
3
declare( strict_types = 1 );
4
5
namespace Diff\Differ;
6
7
use Diff\Comparer\StrictComparer;
8
use Diff\Comparer\ValueComparer;
9
use Diff\DiffOp\Diff\Diff;
10
use Diff\DiffOp\DiffOp;
11
use Diff\DiffOp\DiffOpAdd;
12
use Diff\DiffOp\DiffOpChange;
13
use Diff\DiffOp\DiffOpRemove;
14
use Exception;
15
use LogicException;
16
17
/**
18
 * Differ that does an associative diff between two arrays,
19
 * with the option to do this recursively.
20
 *
21
 * @since 0.4
22
 *
23
 * @license GPL-2.0+
24
 * @author Jeroen De Dauw < [email protected] >
25
 */
26
class MapDiffer implements Differ, MapDifferInterface {
27
28
	/**
29
	 * @var bool
30
	 */
31
	private $recursively;
32
33
	/**
34
	 * @var Differ
35
	 */
36
	private $listDiffer;
37
38
	/**
39
	 * @var ValueComparer
40
	 */
41
	private $valueComparer;
42
43
	/**
44
	 * Create differ for recursive diffs
45
	 */
46
	public static function createRecursive( ValueComparer $comparer = null, Differ $listDiffer = null ) {
47
		$differ = new self( true, null, $comparer );
48
		$differ->listDiffer = $listDiffer ?? $differ;
49
50
		return $differ;
51
	}
52
53
	/**
54
	 * The third argument ($comparer) was added in 3.0
55
	 */
56 27
	public function __construct( bool $recursively = false, Differ $listDiffer = null, ValueComparer $comparer = null ) {
57 27
		$this->recursively = $recursively;
58 27
		$this->listDiffer = $listDiffer ?? new ListDiffer();
59 27
		$this->valueComparer = $comparer ?? new StrictComparer();
60 27
	}
61
62
	/**
63
	 * @see Differ::doDiff
64
	 *
65
	 * Computes the diff between two associate arrays.
66
	 *
67
	 * @since 0.4
68
	 *
69
	 * @param array $oldValues The first array
70
	 * @param array $newValues The second array
71
	 *
72
	 * @throws Exception
73
	 * @return DiffOp[]
74
	 */
75 27
	public function doDiff( array $oldValues, array $newValues ): array {
76 27
		$newSet = $this->arrayDiffAssoc( $newValues, $oldValues );
77 27
		$oldSet = $this->arrayDiffAssoc( $oldValues, $newValues );
78
79 27
		$diffSet = [];
80
81 27
		foreach ( $this->getAllKeys( $oldSet, $newSet ) as $key ) {
82 18
			$diffOp = $this->getDiffOpForElement( $key, $oldSet, $newSet );
83
84 18
			if ( $diffOp !== null ) {
85 18
				$diffSet[$key] = $diffOp;
86
			}
87
		}
88
89 27
		return $diffSet;
90
	}
91
92 27
	private function getAllKeys( array $oldSet, array $newSet ): array {
93 27
		return array_unique( array_merge(
94 27
			array_keys( $oldSet ),
95 27
			array_keys( $newSet )
96
		) );
97
	}
98
99 18
	private function getDiffOpForElement( $key, array $oldSet, array $newSet ) {
100 18
		if ( $this->recursively ) {
101 10
			$diffOp = $this->getDiffOpForElementRecursively( $key, $oldSet, $newSet );
102
103 10
			if ( $diffOp !== null ) {
104 10
				if ( $diffOp->isEmpty() ) {
105
					// there is no (relevant) difference
106 2
					return null;
107
				} else {
108 9
					return $diffOp;
109
				}
110
			}
111
		}
112
113 13
		$hasOld = array_key_exists( $key, $oldSet );
114 13
		$hasNew = array_key_exists( $key, $newSet );
115
116 13
		if ( $hasOld && $hasNew ) {
117 7
			return new DiffOpChange( $oldSet[$key], $newSet[$key] );
118
		}
119 7
		elseif ( $hasOld ) {
120 1
			return new DiffOpRemove( $oldSet[$key] );
121
		}
122 7
		elseif ( $hasNew ) {
123 7
			return new DiffOpAdd( $newSet[$key] );
124
		}
125
126
		// @codeCoverageIgnoreStart
127
		throw new LogicException( 'The element needs to exist in either the old or new list to compare' );
128
		// @codeCoverageIgnoreEnd
129
	}
130
131 10
	private function getDiffOpForElementRecursively( $key, array $oldSet, array $newSet ) {
132 10
		$old = array_key_exists( $key, $oldSet ) ? $oldSet[$key] : [];
133 10
		$new = array_key_exists( $key, $newSet ) ? $newSet[$key] : [];
134
135 10
		if ( is_array( $old ) && is_array( $new ) ) {
136 10
			return $this->getDiffForArrays( $old, $new );
137
		}
138
139 5
		return null;
140
	}
141
142 10
	private function getDiffForArrays( array $old, array $new ): Diff {
143 10
		if ( $this->isAssociative( $old ) || $this->isAssociative( $new ) ) {
144 7
			return new Diff( $this->doDiff( $old, $new ), true );
145
		}
146
147 6
		return new Diff( $this->listDiffer->doDiff( $old, $new ), $this->listDiffer instanceof MapDifferInterface );
148
	}
149
150
	/**
151
	 * Returns if an array is associative or not.
152
	 *
153
	 * @param array $array
154
	 *
155
	 * @return bool
156
	 */
157 10
	private function isAssociative( array $array ): bool {
158 10
		foreach ( $array as $key => $value ) {
159 10
			if ( is_string( $key ) ) {
160 10
				return true;
161
			}
162
		}
163
164 7
		return false;
165
	}
166
167
	/**
168
	 * Similar to the native array_diff_assoc function, except that it will
169
	 * spot differences between array values. Very weird the native
170
	 * function just ignores these...
171
	 *
172
	 * @see http://php.net/manual/en/function.array-diff-assoc.php
173
	 *
174
	 * @param array $from
175
	 * @param array $to
176
	 *
177
	 * @return array
178
	 */
179 27
	private function arrayDiffAssoc( array $from, array $to ): array {
180 27
		$diff = [];
181
182 27
		foreach ( $from as $key => $value ) {
183 26
			if ( !array_key_exists( $key, $to ) || !$this->valueComparer->valuesAreEqual( $to[$key], $value ) ) {
184 26
				$diff[$key] = $value;
185
			}
186
		}
187
188 27
		return $diff;
189
	}
190
191
}
192