Passed
Pull Request — v3 (#729)
by
unknown
60:56 queued 25:59
created

Arr::set()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 23
rs 10
cc 3
nc 3
nop 3
1
<?php
2
/**
3
 * This file is part of GameQ.
4
 *
5
 * GameQ is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU Lesser General Public License as published by
7
 * the Free Software Foundation; either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * GameQ is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Lesser General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Lesser General Public License
16
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18
19
namespace GameQ\Helpers;
20
21
use Closure;
22
use RecursiveArrayIterator;
23
use RecursiveIteratorIterator;
24
25
/**
26
 * This helper contains functions to work with arrays.
27
 *
28
 * @package GameQ\Helpers
29
 */
30
class Arr
31
{
32
    /**
33
     * This helper does process each element of the provided array recursively.
34
     * It does so allowing for modifications to the provided array and without
35
     * using actual recursive calls.
36
     *
37
     * @param array $data
38
     * @param Closure $callback
39
     *
40
     * @return array
41
     */
42
    public static function recursively(array $data, Closure $callback)
43
    {
44
        /* Initialize the RecursiveArrayIterator for the provided data */
45
        $arrayIterator = new RecursiveArrayIterator($data);
46
47
        /* Configure the Iterator for the RecursiveIterator */
48
        $recursiveIterator = new RecursiveIteratorIterator($arrayIterator);
49
50
        /* Traverse the provided data */
51
        foreach ($recursiveIterator as $key => $value) {
52
            /* Get the current sub iterator with Type hinting */
53
            /** @var RecursiveArrayIterator */
54
            $subIterator = $recursiveIterator->getSubIterator();
55
56
            /* Wrap the implementation to handle PHP < 8.1 behaviour */
57
            static::handleArrayIteratorCopyOrReference(
58
                $data,
59
                $recursiveIterator,
60
                $subIterator,
61
                /* Update the current value */
62
                fn () => $subIterator->offsetSet(
0 ignored issues
show
Bug introduced by
The method offsetSet() does not exist on RecursiveIterator. It seems like you code against a sub-type of RecursiveIterator such as Phar or RecursiveCachingIterator or SimpleXMLElement or RecursiveArrayIterator or SimpleXMLIterator or Phar. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

62
                fn () => $subIterator->/** @scrutinizer ignore-call */ offsetSet(
Loading history...
63
                    /* Keep the original key */
64
                    $key,
65
                    /* Execute the callback and use the return / modified value */
66
                    $callback($value, $key, $subIterator) ?? $value
67
                )
68
            );
69
        }
70
71
        /* Return the processed data */
72
        return $data;
73
    }
74
75
    /**
76
     * This function is responsible for handling behaivour specific to PHP versions before 8.1.
77
     *
78
     * @param array &$data
79
     * @param RecursiveIteratorIterator $recursiveIterator
80
     * @param RecursiveArrayIterator $iterator
81
     * @param Closure $callback
82
     * @return void
83
     */
84
    protected static function handleArrayIteratorCopyOrReference(
85
        array &$data,
86
        RecursiveIteratorIterator $recursiveIterator,
87
        RecursiveArrayIterator $iterator,
88
        Closure $callback
89
    ) {
90
        /* ArrayIterator before PHP 8.1 does use a copy instead of reference */
91
        if (PHP_VERSION_ID < 80100) {
92
            /* Hash the current state of the iterator */
93
            $hashes = static::hashes((array) $iterator);
94
95
            /* Continue with the provided callback */
96
            $callback();
97
98
            /* Determine if the current iterator has been modified */
99
            if (! empty($diff = array_diff_assoc(static::hashes((array) $iterator), $hashes))) {
100
                /* Determine path to the current iterator */
101
                $path = [];
102
                for ($depth = 0; $depth < $recursiveIterator->getDepth(); $depth++) {
103
                    $path[] = $recursiveIterator->getSubIterator($depth)->key();
104
                }
105
106
                /* Process all modified values */
107
                foreach (array_keys($diff) as $modified) {
108
                    /* Write the modified value to the original array */
109
                    $data = static::set($data, [...$path, $modified], $iterator->offsetGet($modified));
110
                }
111
            }
112
        } else {
113
            /* There is no need to write back any changes when ArrayIterator does use a reference */
114
            $callback();
115
        }
116
    }
117
118
    /**
119
     * This helper is intended to hash the provided array's values
120
     * and return it back as key => hash.
121
     *
122
     * @param array $array
123
     * @return array<string|int, string>
124
     */
125
    public static function hashes(array $array)
126
    {
127
        $hashes = [];
128
129
        /* Process the provided array */
130
        foreach ($array as $key => $value) {
131
            /* Serialze and hash each value individually */
132
            $hashes[$key] = md5(serialize($value));
133
        }
134
135
        /* Return array containing the hashes */
136
        return $hashes;
137
    }
138
139
    /**
140
     * This helper is intended to set a value inside the provided array.
141
     *
142
     * @param array &$array
143
     * @param array $path
144
     * @param mixed $value
145
     * @return array
146
     */
147
    public static function set(array &$array, array $path, $value)
148
    {
149
        $current = &$array;
150
151
        /* Process the path until the last element */
152
        foreach ($path as $i => $element) {
153
            /* Remove the element from the path */
154
            unset($path[$i]);
155
156
            /* Create missing key */
157
            if (! isset($current[$element])) {
158
                $current[$element] = [];
159
            }
160
161
            /* Set current to a reference of next */
162
            $current = &$current[$element];
163
        }
164
165
        /* Finally set the value using the last key */
166
        $current = $value;
167
168
        /* Return the current, modified array (level) */
169
        return $array;
170
    }
171
}
172