Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.44% covered (success)
94.44%
34 / 36
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZObjectDiffer
94.44% covered (success)
94.44%
34 / 36
60.00% covered (warning)
60.00%
3 / 5
16.04
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 doDiff
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 getDifferType
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isAssociative
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 flattenDiff
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * WikiLambda ZObjectDiffer. Differ service entrypoint, implements doDiff on
4 * any kind of ZObject. Depending on the types, uses ZObjectMapDiffer or
5 * ZObjectListDiffer.
6 *
7 * @file
8 * @ingroup Extensions
9 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
10 * @license MIT
11 */
12
13namespace MediaWiki\Extension\WikiLambda\Diff;
14
15use Diff\Comparer\StrictComparer;
16use Diff\Differ\Differ;
17use Diff\DiffOp\Diff\Diff;
18use Diff\DiffOp\DiffOp;
19use Diff\DiffOp\DiffOpChange;
20use Exception;
21
22class ZObjectDiffer {
23
24    private const DIFF_STRING = 1;
25    private const DIFF_ARRAY = 2;
26    private const DIFF_ASSOCIATIVE = 3;
27
28    private ZObjectListDiffer $listDiffer;
29    private ZObjectMapDiffer $mapDiffer;
30    private StrictComparer $comparer;
31
32    public function __construct() {
33        // Non-injected items
34        $this->comparer = new StrictComparer();
35        $this->listDiffer = new ZObjectListDiffer();
36        $this->mapDiffer = new ZObjectMapDiffer( $this->listDiffer, $this->comparer );
37        $this->listDiffer->setZObjectDiffer( $this );
38    }
39
40    /**
41     * @see Differ::doDiff
42     *
43     * Takes two ZObjects, computes the diff, and returns this diff as an array of DiffOp.
44     *
45     * @param array|string $oldValues The first array
46     * @param array|string $newValues The second array
47     *
48     * @throws Exception
49     * @return DiffOp returns either an atomic DiffOp or a new
50     */
51    public function doDiff( $oldValues, $newValues ): DiffOp {
52        $oldDiffer = $this->getDifferType( $oldValues );
53        $newDiffer = $this->getDifferType( $newValues );
54
55        if ( $oldDiffer !== $newDiffer ) {
56            // If the type is different, register a DiffOpChange
57            return new DiffOpChange( $oldValues, $newValues );
58        } elseif ( $oldDiffer === self::DIFF_ASSOCIATIVE ) {
59            // If the items are associative arrays, call ZObjectMapDiffer::doDiff
60            return new Diff( $this->mapDiffer->doDiff( $oldValues, $newValues ) );
61        } elseif ( $oldDiffer === self::DIFF_ARRAY ) {
62            // If the items are non-associative arrays, call ZObjectListDiffer::doDiff
63            return new Diff( $this->listDiffer->doDiff( $oldValues, $newValues ), true );
64        } else {
65            // If the items are strings and not equal, register a DiffOpChange
66            if ( !$this->comparer->valuesAreEqual( $oldValues, $newValues ) ) {
67                return new DiffOpChange( $oldValues, $newValues );
68            }
69        }
70
71        // Return an empty diff
72        return new Diff( [] );
73    }
74
75    /**
76     * Returns the type of differ that we should use for a given input.
77     *
78     * @param array|string $input
79     * @return int
80     */
81    protected function getDifferType( $input ): int {
82        if ( is_array( $input ) ) {
83            return $this->isAssociative( $input )
84                ? self::DIFF_ASSOCIATIVE
85                : self::DIFF_ARRAY;
86        }
87        return self::DIFF_STRING;
88    }
89
90    /**
91     * Returns if an array is associative or not by checking whether
92     * any of its keys is a string key (and cannot be cast to an int)
93     *
94     * @param array $array
95     * @return bool
96     */
97    private function isAssociative( array $array ): bool {
98        foreach ( $array as $key => $value ) {
99            if ( is_string( $key ) ) {
100                return true;
101            }
102        }
103        return false;
104    }
105
106    /**
107     * Returns a flat collection of diffs with an absolute path and the DiffOp
108     * that has been detected under that path.
109     *
110     * @param DiffOp $diff
111     * @return array
112     */
113    public static function flattenDiff( $diff ): array {
114        // Finish condition when the $diff is an atomic DiffOp
115        if ( $diff->isAtomic() ) {
116            return [ [
117                'path' => [],
118                'op' =>    $diff
119            ] ];
120        }
121
122        // Else prepend the key to the path and return a flattened array of DiffOps
123        // If it's not atomic, then $diff must be an instanceof Diff
124        '@phan-var Diff $diff';
125        $branches = [];
126        foreach ( $diff->getOperations() as $key => $diffOp ) {
127            $flatOps = self::flattenDiff( $diffOp );
128            for ( $index = 0; $index < count( $flatOps ); $index++ ) {
129                array_unshift( $flatOps[$index]['path'], $key );
130            }
131            $branches = array_merge( $branches, $flatOps );
132        }
133        return $branches;
134    }
135
136}