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