Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.00% covered (success)
95.00%
38 / 40
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZObjectMapDiffer
95.00% covered (success)
95.00%
38 / 40
85.71% covered (warning)
85.71%
6 / 7
23
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 doDiff
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getAllKeys
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getDiffOpForElement
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
8.19
 getDiffForArrays
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 isAssociative
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 arrayDiffAssoc
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * WikiLambda ZObjectMapDiffer. Implements doDiff to calculate the diff
4 * between two associative arrays, or maps.
5 *
6 * @file
7 * @ingroup Extensions
8 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
9 * @license MIT
10 */
11
12namespace MediaWiki\Extension\WikiLambda\Diff;
13
14use Diff\Comparer\ValueComparer;
15use Diff\DiffOp\Diff\Diff;
16use Diff\DiffOp\DiffOp;
17use Diff\DiffOp\DiffOpAdd;
18use Diff\DiffOp\DiffOpChange;
19use Diff\DiffOp\DiffOpRemove;
20use Exception;
21
22class ZObjectMapDiffer {
23
24    private ZObjectListDiffer $listDiffer;
25    private ValueComparer $valueComparer;
26
27    /**
28     * Creates a ZObjectMapDiffer object
29     *
30     * @param ZObjectListDiffer $listDiffer
31     * @param ValueComparer $comparer
32     */
33    public function __construct( ZObjectListDiffer $listDiffer, ValueComparer $comparer ) {
34        $this->listDiffer = $listDiffer;
35        $this->valueComparer = $comparer;
36    }
37
38    /**
39     * Computes the diff between two ZObject associate arrays.
40     *
41     * @param array $oldValues The first array
42     * @param array $newValues The second array
43     *
44     * @throws Exception
45     * @return DiffOp[]
46     */
47    public function doDiff( array $oldValues, array $newValues ): array {
48        $newSet = $this->arrayDiffAssoc( $newValues, $oldValues );
49        $oldSet = $this->arrayDiffAssoc( $oldValues, $newValues );
50
51        $diffSet = [];
52
53        foreach ( $this->getAllKeys( $oldSet, $newSet ) as $key ) {
54            $diffOp = $this->getDiffOpForElement( $key, $oldSet, $newSet );
55
56            if ( $diffOp !== null ) {
57                $diffSet[$key] = $diffOp;
58            }
59        }
60
61        return $diffSet;
62    }
63
64    /**
65     * Returns the union of all keys present in old and new sets
66     *
67     * @param array $oldSet
68     * @param array $newSet
69     * @return string[]
70     */
71    private function getAllKeys( array $oldSet, array $newSet ): array {
72        return array_unique( array_merge(
73            array_keys( $oldSet ),
74            array_keys( $newSet )
75        ) );
76    }
77
78    /**
79     * Returns the DiffOp found for the old and new values of a given key
80     * or null if no diffs were found.
81     *
82     * @param string $key
83     * @param array $oldSet
84     * @param array $newSet
85     * @return DiffOp|null
86     */
87    private function getDiffOpForElement( $key, array $oldSet, array $newSet ) {
88        $hasOld = array_key_exists( $key, $oldSet );
89        $hasNew = array_key_exists( $key, $newSet );
90
91        if ( $hasOld && $hasNew ) {
92            $oldValue = $oldSet[$key];
93            $newValue = $newSet[$key];
94
95            if ( is_array( $oldValue ) && is_array( $newValue ) ) {
96                $diffOp = $this->getDiffForArrays( $oldValue, $newValue );
97                return $diffOp->isEmpty() ? null : $diffOp;
98            } else {
99                return new DiffOpChange( $oldValue, $newValue );
100            }
101        } elseif ( $hasOld ) {
102            return new DiffOpRemove( $oldSet[$key] );
103        } elseif ( $hasNew ) {
104            return new DiffOpAdd( $newSet[$key] );
105        }
106
107        return null;
108    }
109
110    /**
111     * Calculates the Diff between two arrays, calling ZObjectMapDiffer
112     * if the arrays are associative or ZObjectListDiffer if they are not
113     *
114     * @param array $old
115     * @param array $new
116     * @return Diff
117     */
118    private function getDiffForArrays( array $old, array $new ): Diff {
119        if ( $this->isAssociative( $old ) || $this->isAssociative( $new ) ) {
120            return new Diff( $this->doDiff( $old, $new ), true );
121        }
122
123        return new Diff( $this->listDiffer->doDiff( $old, $new ), false );
124    }
125
126    /**
127     * Returns if an array is associative or not.
128     *
129     * @param array $array
130     * @return bool
131     */
132    private function isAssociative( array $array ): bool {
133        foreach ( $array as $key => $value ) {
134            if ( is_string( $key ) ) {
135                return true;
136            }
137        }
138
139        return false;
140    }
141
142    /**
143     * Similar to the native array_diff_assoc function, except that it will
144     * spot differences between array values. Very weird the native
145     * function just ignores these...
146     *
147     * @see http://php.net/manual/en/function.array-diff-assoc.php
148     * @param array $from
149     * @param array $to
150     * @return array
151     */
152    private function arrayDiffAssoc( array $from, array $to ): array {
153        $diff = [];
154
155        foreach ( $from as $key => $value ) {
156            if ( !array_key_exists( $key, $to ) || !$this->valueComparer->valuesAreEqual( $to[$key], $value ) ) {
157                $diff[$key] = $value;
158            }
159        }
160
161        return $diff;
162    }
163
164}