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