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        $this->comparer = new StrictComparer();
34        $this->listDiffer = new ZObjectListDiffer();
35        $this->mapDiffer = new ZObjectMapDiffer( $this->listDiffer, $this->comparer );
36        $this->listDiffer->setZObjectDiffer( $this );
37    }
38
39    /**
40     * @see Differ::doDiff
41     *
42     * Takes two ZObjects, computes the diff, and returns this diff as an array of DiffOp.
43     *
44     * @param array|string $oldValues The first array
45     * @param array|string $newValues The second array
46     *
47     * @throws Exception
48     * @return DiffOp returns either an atomic DiffOp or a new
49     */
50    public function doDiff( $oldValues, $newValues ): DiffOp {
51        $oldDiffer = $this->getDifferType( $oldValues );
52        $newDiffer = $this->getDifferType( $newValues );
53
54        if ( $oldDiffer !== $newDiffer ) {
55            // If the type is different, register a DiffOpChange
56            return new DiffOpChange( $oldValues, $newValues );
57        } elseif ( $oldDiffer === self::DIFF_ASSOCIATIVE ) {
58            // If the items are associative arrays, call ZObjectMapDiffer::doDiff
59            return new Diff( $this->mapDiffer->doDiff( $oldValues, $newValues ) );
60        } elseif ( $oldDiffer === self::DIFF_ARRAY ) {
61            // If the items are non-associative arrays, call ZObjectListDiffer::doDiff
62            return new Diff( $this->listDiffer->doDiff( $oldValues, $newValues ), true );
63        } else {
64            // If the items are strings and not equal, register a DiffOpChange
65            if ( !$this->comparer->valuesAreEqual( $oldValues, $newValues ) ) {
66                return new DiffOpChange( $oldValues, $newValues );
67            }
68        }
69
70        // Return an empty diff
71        return new Diff( [] );
72    }
73
74    /**
75     * Returns the type of differ that we should use for a given input.
76     *
77     * @param array|string $input
78     * @return int
79     */
80    protected function getDifferType( $input ): int {
81        if ( is_array( $input ) ) {
82            return $this->isAssociative( $input )
83                ? self::DIFF_ASSOCIATIVE
84                : self::DIFF_ARRAY;
85        }
86        return self::DIFF_STRING;
87    }
88
89    /**
90     * Returns if an array is associative or not.
91     *
92     * @param array $array
93     * @return bool
94     */
95    private function isAssociative( array $array ): bool {
96        foreach ( $array as $key => $value ) {
97            if ( is_string( $key ) ) {
98                return true;
99            }
100        }
101        return false;
102    }
103
104    /**
105     * Returns a flat collection of diffs with an absolute path and the DiffOp
106     * that has been detected under that path.
107     *
108     * @param DiffOp $diff
109     * @return array
110     */
111    public static function flattenDiff( $diff ): array {
112        // Finish condition when the $diff is an atomic DiffOp
113        if ( $diff->isAtomic() ) {
114            return [ [
115                'path' => [],
116                'op' =>    $diff
117            ] ];
118        }
119
120        // Else prepend the key to the path and return a flattened array of DiffOps
121        // If it's not atomic, then $diff must be an instanceof Diff
122        '@phan-var Diff $diff';
123        $branches = [];
124        foreach ( $diff->getOperations() as $key => $diffOp ) {
125            $flatOps = self::flattenDiff( $diffOp );
126            for ( $index = 0; $index < count( $flatOps ); $index++ ) {
127                array_unshift( $flatOps[$index]['path'], $key );
128            }
129            $branches = array_merge( $branches, $flatOps );
130        }
131        return $branches;
132    }
133
134}