├── CONTRIBUTING.md ├── LICENSE ├── composer.json └── src └── Flow └── ArrayDot ├── Exception ├── Exception.php └── InvalidPathException.php └── array_dot.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | This repo is **READ ONLY**, in order to contribute to Flow PHP project, please 4 | open PR against [flow](https://github.com/flow-php/flow) monorepo. 5 | 6 | Changes merged to monorepo are automatically propagated into sub repositories. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-present Flow PHP 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-php/array-dot", 3 | "type": "library", 4 | "description": "PHP ETL - Array Dot functions", 5 | "keywords": [ 6 | "etl", 7 | "extract", 8 | "transform", 9 | "load", 10 | "filter", 11 | "array", 12 | "dot" 13 | ], 14 | "require": { 15 | "php": "~8.2.0 || ~8.3.0 || ~8.4.0" 16 | }, 17 | "config": { 18 | "optimize-autoloader": true, 19 | "sort-packages": true 20 | }, 21 | "license": "MIT", 22 | "autoload": { 23 | "psr-4": { 24 | "Flow\\": [ 25 | "src/Flow" 26 | ] 27 | }, 28 | "files": [ 29 | "src/Flow/ArrayDot/array_dot.php" 30 | ] 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Flow\\": "tests/Flow" 35 | } 36 | }, 37 | "minimum-stability": "dev", 38 | "prefer-stable": true 39 | } 40 | -------------------------------------------------------------------------------- /src/Flow/ArrayDot/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | function array_dot_steps(string $path) : array 17 | { 18 | if ('' === $path) { 19 | throw new InvalidPathException("Path can't be empty."); 20 | } 21 | 22 | if (\str_contains($path, '{')) { 23 | if (!\str_contains($path, '}')) { 24 | throw new InvalidPathException('Multimatch syntax not closed'); 25 | } 26 | 27 | if (\strpos($path, '}') !== \strlen($path) - 1) { 28 | throw new InvalidPathException('Multimatch must be used at the end of path'); 29 | } 30 | } 31 | 32 | $path = \str_replace('\\.', '__ESCAPED_DOT__', $path); 33 | 34 | if (\preg_match('/(\.)({(.*?)})/', $path, $multiMatchPath)) { 35 | $path = \str_replace($multiMatchPath[2], '__MULTIMATCH_PATH__', $path); 36 | } 37 | 38 | if (\str_starts_with($path, '{') && \str_contains($path, '}')) { 39 | $pathSteps = [$path]; 40 | } else { 41 | $pathSteps = \explode('.', $path); 42 | } 43 | 44 | foreach ($pathSteps as $index => $step) { 45 | $pathSteps[$index] = \str_replace('__ESCAPED_DOT__', '.', $step); 46 | 47 | if ($step === '__MULTIMATCH_PATH__') { 48 | /** @phpstan-ignore-next-line */ 49 | $pathSteps[$index] = $multiMatchPath[2]; 50 | } 51 | } 52 | 53 | return $pathSteps; 54 | } 55 | 56 | /** 57 | * @param array $array 58 | * @param string $path 59 | * @param mixed $value 60 | * 61 | * @throws InvalidPathException 62 | * 63 | * @return array 64 | */ 65 | function array_dot_set(array $array, string $path, $value) : array 66 | { 67 | $pathSteps = array_dot_steps($path); 68 | 69 | $newArray = []; 70 | $currentElement = &$newArray; 71 | 72 | $takenSteps = []; 73 | 74 | foreach ($pathSteps as $step) { 75 | $takenSteps[] = $step; 76 | 77 | if ($step === '*') { 78 | /** 79 | * @var array $nestedValues 80 | */ 81 | $nestedValues = array_dot_get($array, \implode('.', $takenSteps)); 82 | $stepsLeft = \array_slice($pathSteps, \count($takenSteps), \count($pathSteps)); 83 | 84 | /** @var mixed $nestedValue */ 85 | foreach ($nestedValues as $nestedKey => $nestedValue) { 86 | $currentElement[$nestedKey] = array_dot_set((array) $nestedValue, \implode('.', $stepsLeft), $value); 87 | } 88 | 89 | return $newArray; 90 | } 91 | 92 | if ($step == '\\*') { 93 | $step = \str_replace('\\', '', $step); 94 | \array_pop($takenSteps); 95 | $takenSteps[] = $step; 96 | } 97 | 98 | $currentElement[$step] = []; 99 | 100 | $currentElement = &$currentElement[$step]; 101 | } 102 | 103 | $currentElement = $value; 104 | 105 | /** @var array $newArray */ 106 | return \array_merge($array, $newArray); 107 | } 108 | 109 | function array_dot_get_int(array $array, string $path) : ?int 110 | { 111 | $result = array_dot_get($array, $path); 112 | 113 | if ($result === null) { 114 | return null; 115 | } 116 | 117 | return (int) $result; 118 | } 119 | 120 | function array_dot_get_string(array $array, string $path) : ?string 121 | { 122 | $result = array_dot_get($array, $path); 123 | 124 | if ($result === null) { 125 | return null; 126 | } 127 | 128 | return (string) $result; 129 | } 130 | 131 | function array_dot_get_bool(array $array, string $path) : ?bool 132 | { 133 | $result = array_dot_get($array, $path); 134 | 135 | if ($result === null) { 136 | return null; 137 | } 138 | 139 | return (bool) $result; 140 | } 141 | 142 | function array_dot_get_float(array $array, string $path) : ?float 143 | { 144 | $result = array_dot_get($array, $path); 145 | 146 | if ($result === null) { 147 | return null; 148 | } 149 | 150 | return (float) $result; 151 | } 152 | 153 | function array_dot_get_datetime(array $array, string $path) : ?\DateTimeImmutable 154 | { 155 | $result = array_dot_get($array, $path); 156 | 157 | if ($result === null) { 158 | return null; 159 | } 160 | 161 | return new \DateTimeImmutable($result); 162 | } 163 | 164 | /** 165 | * @template T is \BackedEnum 166 | * 167 | * @param array $array 168 | * @param string $path 169 | * @param class-string $enumClass 170 | * 171 | * @return null|\BackedEnum 172 | */ 173 | function array_dot_get_enum(array $array, string $path, string $enumClass) : ?\BackedEnum 174 | { 175 | if (!\class_exists($enumClass)) { 176 | throw new Exception('Enum class does not exist'); 177 | } 178 | 179 | if (!\is_subclass_of($enumClass, \BackedEnum::class)) { 180 | throw new Exception('Enum class must be subclass of BackedEnum'); 181 | } 182 | 183 | $reflection = new \ReflectionEnum($enumClass); 184 | 185 | $result = match ((string) $reflection->getBackingType()) { 186 | 'int' => array_dot_get_int($array, $path), 187 | 'string' => array_dot_get_string($array, $path), 188 | default => throw new Exception('Unsupported enum backing type: ' . $reflection->getBackingType()), 189 | }; 190 | 191 | if ($result === null) { 192 | return null; 193 | } 194 | 195 | return $enumClass::tryFrom($result); 196 | } 197 | 198 | /** 199 | * @param array $array 200 | * @param string $path 201 | * 202 | * @throws InvalidPathException 203 | * 204 | * @return mixed 205 | */ 206 | function array_dot_get(array $array, string $path) : mixed 207 | { 208 | if ([] === $array) { 209 | if (\str_starts_with($path, '?')) { 210 | return null; 211 | } 212 | 213 | throw new InvalidPathException( 214 | \sprintf( 215 | 'Path "%s" does not exists in array "%s".', 216 | $path, 217 | \preg_replace('/\s+/', '', \trim(\var_export($array, true))) 218 | ) 219 | ); 220 | } 221 | 222 | $pathSteps = array_dot_steps($path); 223 | 224 | $arraySlice = $array; 225 | /** @var array $takenSteps */ 226 | $takenSteps = []; 227 | 228 | foreach ($pathSteps as $step) { 229 | $takenSteps[] = $step; 230 | 231 | if (\in_array($step, ['*', '?*'], true)) { 232 | $stepsLeft = \array_slice($pathSteps, \count($takenSteps), \count($pathSteps)); 233 | $results = []; 234 | 235 | foreach (\array_keys($arraySlice) as $key) { 236 | if (!\count($stepsLeft)) { 237 | return $arraySlice; 238 | } 239 | 240 | if ($step === '?*') { 241 | if (!\is_array($arraySlice[$key])) { 242 | $pathTaken = \implode('.', $takenSteps); 243 | $type = \gettype($arraySlice[$key]); 244 | 245 | throw new InvalidPathException("Expected array under path, \"{$pathTaken}\", but got: {$type}"); 246 | } 247 | 248 | if (array_dot_exists($arraySlice[$key], \implode('.', $stepsLeft))) { 249 | $results[] = array_dot_get($arraySlice[$key], \implode('.', $stepsLeft)); 250 | } 251 | } else { 252 | if (!\is_array($arraySlice[$key])) { 253 | $pathTaken = \implode('.', $takenSteps); 254 | $type = \gettype($arraySlice[$key]); 255 | 256 | throw new InvalidPathException("Expected array under path, \"{$pathTaken}\", but got: {$type}"); 257 | } 258 | 259 | $results[] = array_dot_get($arraySlice[$key], \implode('.', $stepsLeft)); 260 | } 261 | } 262 | 263 | return $results; 264 | } 265 | 266 | // Multiselect 267 | if (\preg_match('/^{(.*?)}$/', $step, $subSteps)) { 268 | $subSteps = \explode(',', $subSteps[1]); 269 | $results = []; 270 | 271 | foreach ($subSteps as $subStep) { 272 | $subSteps = array_dot_steps(\trim($subStep)); 273 | 274 | $results[\str_replace('.', '_', \str_replace('?', '', \trim($subStep)))] = array_dot_get($arraySlice, \trim($subStep)); 275 | } 276 | 277 | return $results; 278 | } 279 | 280 | if (\in_array($step, ['\\*', '\\?*'], true)) { 281 | $step = \ltrim($step, '\\'); 282 | \array_pop($takenSteps); 283 | $takenSteps[] = $step; 284 | } 285 | 286 | $nullSafe = false; 287 | 288 | if (\str_starts_with($step, '?') && $step !== '?*') { 289 | $nullSafe = true; 290 | $step = \ltrim($step, '?'); 291 | \array_pop($takenSteps); 292 | $takenSteps[] = $step; 293 | } 294 | 295 | if (\str_contains($step, '\\{')) { 296 | $step = \str_replace('\\{', '{', $step); 297 | \array_pop($takenSteps); 298 | $takenSteps[] = $step; 299 | } 300 | 301 | if (\str_contains($step, '\\}')) { 302 | $step = \str_replace('\\}', '}', $step); 303 | \array_pop($takenSteps); 304 | $takenSteps[] = $step; 305 | } 306 | 307 | if (!\array_key_exists($step, $arraySlice)) { 308 | if (!$nullSafe) { 309 | throw new InvalidPathException( 310 | \sprintf( 311 | 'Path "%s" does not exists in array "%s".', 312 | $path, 313 | \preg_replace('/\s+/', '', \trim(\var_export($array, true))) 314 | ) 315 | ); 316 | } 317 | 318 | return null; 319 | } 320 | 321 | /** @var array $arraySlice */ 322 | $arraySlice = $arraySlice[$step]; 323 | } 324 | 325 | return $arraySlice; 326 | } 327 | 328 | /** 329 | * @param array $array 330 | * @param string $path 331 | * @param string $newName 332 | * 333 | * @throws InvalidPathException 334 | * 335 | * @return array 336 | */ 337 | function array_dot_rename(array $array, string $path, string $newName) : array 338 | { 339 | if (!array_dot_exists($array, $path)) { 340 | throw new InvalidPathException( 341 | \sprintf( 342 | 'Path "%s" does not exists in array "%s".', 343 | $path, 344 | \preg_replace('/\s+/', '', \trim(\var_export($array, true))) 345 | ) 346 | ); 347 | } 348 | 349 | $pathSteps = array_dot_steps($path); 350 | $lastStep = \array_pop($pathSteps); 351 | 352 | $currentElement = &$array; 353 | 354 | $takenSteps = []; 355 | 356 | foreach ($pathSteps as $step) { 357 | $takenSteps[] = $step; 358 | 359 | if ($step === '*') { 360 | /** 361 | * @var array $nestedValues 362 | */ 363 | $nestedValues = array_dot_get($array, \implode('.', $takenSteps)); 364 | $stepsLeft = \array_slice($pathSteps, \count($takenSteps), \count($pathSteps)); 365 | $stepsLeft[] = $lastStep; 366 | 367 | /** @var mixed $nestedValue */ 368 | foreach ($nestedValues as $nestedKey => $nestedValue) { 369 | $currentElement[$nestedKey] = array_dot_rename((array) $nestedValue, \implode('.', $stepsLeft), $newName); 370 | } 371 | 372 | return $array; 373 | } 374 | 375 | if ($step == '\\*') { 376 | $step = \str_replace('\\', '', $step); 377 | \array_pop($takenSteps); 378 | $takenSteps[] = $step; 379 | } 380 | 381 | if (!\is_array($currentElement[$step])) { 382 | throw new Exception( 383 | \sprintf( 384 | 'Item for path "%s" is not an array in "%s".', 385 | \implode('.', $takenSteps), 386 | \preg_replace('/\s+/', '', \trim(\var_export($array, true))) 387 | ) 388 | ); 389 | } 390 | 391 | $currentElement = &$currentElement[$step]; 392 | } 393 | 394 | $currentElement[$newName] = $currentElement[$lastStep]; 395 | unset($currentElement[$lastStep]); 396 | 397 | return $array; 398 | } 399 | 400 | /** 401 | * @param array $array 402 | * @param string $path 403 | * 404 | * @return bool 405 | */ 406 | function array_dot_exists(array $array, string $path) : bool 407 | { 408 | try { 409 | array_dot_get($array, $path); 410 | 411 | return true; 412 | } catch (InvalidPathException) { 413 | return false; 414 | } 415 | } 416 | --------------------------------------------------------------------------------