Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
ConfigHelper
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
3 / 3
19
100.00% covered (success)
100.00%
1 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 isAssoc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 normalize
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
13
 flatten
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of php-fast-forward/config.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @link      https://github.com/php-fast-forward/config
12 * @copyright Copyright (c) 2025 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
13 * @license   https://opensource.org/licenses/MIT MIT License
14 */
15
16namespace FastForward\Config\Helper;
17
18use Dflydev\DotAccessData\Data;
19use Dflydev\DotAccessData\DataInterface;
20use Dflydev\DotAccessData\Util;
21
22/**
23 * Class ConfigHelper.
24 *
25 * Provides a set of static helper methods for manipulating configuration arrays,
26 * particularly handling associative arrays with dot notation and nested structures.
27 * This class SHALL NOT be instantiated and MUST be used statically.
28 */
29final class ConfigHelper
30{
31    /**
32     * ConfigHelper constructor.
33     *
34     * This constructor is private to prevent instantiation of the class.
35     * The class MUST be used in a static context only.
36     *
37     * @codeCoverageIgnore
38     */
39    private function __construct()
40    {
41        // Prevent instantiation
42    }
43
44    /**
45     * Determines if the provided value is an associative array.
46     *
47     * This method SHALL check whether the given array uses string keys,
48     * distinguishing it from indexed arrays.
49     *
50     * @param mixed $value the value to check
51     *
52     * @return bool true if the array is associative; false otherwise
53     */
54    public static function isAssoc(mixed $value): bool
55    {
56        return \is_array($value) && Util::isAssoc($value);
57    }
58
59    /**
60     * Normalizes a configuration array using dot notation delimiters.
61     *
62     * This method SHALL recursively convert keys containing delimiters into nested arrays.
63     * For example, a key like "database.host" SHALL be transformed into
64     * ['database' => ['host' => 'value']].
65     *
66     * @param array $config the configuration array to normalize
67     *
68     * @return array the normalized configuration array
69     */
70    public static function normalize(array $config): array
71    {
72        if (!self::isAssoc($config)) {
73            return $config;
74        }
75
76        $normalized = [];
77
78        $reflectionConst = new \ReflectionClassConstant(Data::class, 'DELIMITERS');
79        $delimiters      = $reflectionConst->getValue();
80
81        $delimiterChars    = implode('', $delimiters);
82        $delimitersPattern = '/[' . preg_quote($delimiterChars, '/') . ']/';
83
84        foreach ($config as $key => $value) {
85            if (self::isAssoc($value)) {
86                $value = self::normalize($value);
87            }
88
89            if (!\is_string($key) || false === strpbrk($key, $delimiterChars)) {
90                $normalized[$key] = $value;
91
92                continue;
93            }
94
95            $parts     = preg_split($delimitersPattern, $key);
96            $current   = &$normalized;
97            $lastIndex = \count($parts) - 1;
98
99            foreach ($parts as $index => $part) {
100                if ($index !== $lastIndex) {
101                    if (!isset($current[$part]) || !\is_array($current[$part])) {
102                        $current[$part] = [];
103                    }
104                    $current = &$current[$part];
105
106                    continue;
107                }
108
109                if (isset($current[$part]) && \is_array($current[$part]) && \is_array($value)) {
110                    $current[$part] = Util::mergeAssocArray($current[$part], $value, DataInterface::MERGE);
111
112                    continue;
113                }
114
115                $current[$part] = $value;
116            }
117        }
118
119        return $normalized;
120    }
121
122    /**
123     * Flattens a nested configuration array into a dot-notated traversable set.
124     *
125     * This method SHALL recursively iterate through the nested array structure
126     * and convert it into a flat representation where keys reflect the nested path.
127     *
128     * For example:
129     * Input: ['database' => ['host' => 'localhost']]
130     * Output: ['database.host' => 'localhost']
131     *
132     * @param array  $config  the configuration array to flatten
133     * @param string $rootKey (Optional) The root key prefix for recursive calls
134     *
135     * @return \Traversable<string, mixed> a traversable list of flattened key-value pairs
136     */
137    public static function flatten(array $config, string $rootKey = ''): \Traversable
138    {
139        foreach ($config as $key => $value) {
140            if (\is_array($value)) {
141                yield from self::flatten($value, $rootKey . $key . '.');
142            } else {
143                yield $rootKey . $key => $value;
144            }
145        }
146    }
147}