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