Skip to content

Commit eccc7bb

Browse files
committed
Change getIterator behavior
1 parent 8938517 commit eccc7bb

11 files changed

+359
-72
lines changed

src/ArrayConfig.php

Lines changed: 14 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
namespace FastForward\Config;
1717

1818
use Dflydev\DotAccessData\Data;
19-
use Dflydev\DotAccessData\Util;
2019
use FastForward\Config\Exception\InvalidArgumentException;
20+
use FastForward\Config\Helper\ConfigHelper;
2121

2222
/**
2323
* Class ArrayConfig.
@@ -44,7 +44,9 @@ final class ArrayConfig implements ConfigInterface
4444
*/
4545
public function __construct(array $config = [])
4646
{
47-
$this->data = new Data(data: $this->normalizeConfig($config));
47+
$this->data = new Data(
48+
data: ConfigHelper::normalize($config),
49+
);
4850
}
4951

5052
/**
@@ -74,7 +76,7 @@ public function get(string $key, mixed $default = null): mixed
7476
{
7577
$value = $this->data->get($key, $default);
7678

77-
if (\is_array($value) && Util::isAssoc($value)) {
79+
if (ConfigHelper::isAssoc($value)) {
7880
return new self($value);
7981
}
8082

@@ -106,23 +108,23 @@ public function set(array|ConfigInterface|string $key, mixed $value = null): voi
106108
$key = $key->toArray();
107109
}
108110

109-
if (Util::isAssoc($key)) {
110-
$key = $this->normalizeConfig($key);
111-
}
112-
113-
$this->data->import($key);
111+
$this->data->import(ConfigHelper::normalize($key));
114112
}
115113

116114
/**
117-
* Retrieves an iterator for traversing configuration data.
115+
* Retrieves a traversable set of flattened configuration data.
118116
*
119-
* This method SHALL provide recursive array iteration.
117+
* This method SHALL return an iterator where each key represents
118+
* the nested path in dot notation, and each value is the corresponding value.
120119
*
121-
* @return \Traversable a recursive array iterator instance
120+
* For example:
121+
* ['database' => ['host' => 'localhost']] becomes ['database.host' => 'localhost'].
122+
*
123+
* @return \Traversable<string, mixed> an iterator of flattened key-value pairs
122124
*/
123125
public function getIterator(): \Traversable
124126
{
125-
return new \RecursiveArrayIterator($this->toArray());
127+
return ConfigHelper::flatten($this->toArray());
126128
}
127129

128130
/**
@@ -136,61 +138,4 @@ public function toArray(): array
136138
{
137139
return $this->data->export();
138140
}
139-
140-
/**
141-
* Normalizes a configuration array using dot notation delimiters.
142-
*
143-
* The method SHALL recursively parse keys containing delimiters and convert them into nested arrays.
144-
*
145-
* @param array $config the configuration array to normalize
146-
*
147-
* @return array the normalized configuration array
148-
*/
149-
private function normalizeConfig(array $config): array
150-
{
151-
$normalized = [];
152-
153-
$reflectionConst = new \ReflectionClassConstant(Data::class, 'DELIMITERS');
154-
$delimiters = $reflectionConst->getValue();
155-
156-
$delimiterChars = implode('', $delimiters);
157-
$delimitersPattern = '/[' . preg_quote($delimiterChars, '/') . ']/';
158-
159-
foreach ($config as $key => $value) {
160-
if (\is_array($value) && Util::isAssoc($value)) {
161-
$value = $this->normalizeConfig($value);
162-
}
163-
164-
if (!\is_string($key) || false === strpbrk($key, $delimiterChars)) {
165-
$normalized[$key] = $value;
166-
167-
continue;
168-
}
169-
170-
$parts = preg_split($delimitersPattern, $key);
171-
$current = &$normalized;
172-
$lastIndex = \count($parts) - 1;
173-
174-
foreach ($parts as $index => $part) {
175-
if ($index !== $lastIndex) {
176-
if (!isset($current[$part]) || !\is_array($current[$part])) {
177-
$current[$part] = [];
178-
}
179-
$current = &$current[$part];
180-
181-
continue;
182-
}
183-
184-
if (isset($current[$part]) && \is_array($current[$part]) && \is_array($value)) {
185-
$current[$part] = Util::mergeAssocArray($current[$part], $value, Data::MERGE);
186-
187-
continue;
188-
}
189-
190-
$current[$part] = $value;
191-
}
192-
}
193-
194-
return $normalized;
195-
}
196141
}

src/CachedConfig.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ final class CachedConfig implements ConfigInterface
3838
* @param CacheInterface $cache the cache implementation used for storing configuration data
3939
* @param ConfigInterface $defaultConfig the configuration source to be cached
4040
* @param bool $persistent whether the cache should be persistent or not
41-
* @param string|null $cacheKey the cache key to use for storing the configuration data
41+
* @param null|string $cacheKey the cache key to use for storing the configuration data
4242
*/
4343
public function __construct(
4444
private readonly CacheInterface $cache,
@@ -55,9 +55,9 @@ public function __construct(
5555
* If the configuration has not yet been cached, it MUST be stored in the cache upon first invocation.
5656
* This method MUST return a ConfigInterface implementation containing the cached configuration data.
5757
*
58-
* @throws InvalidArgumentException if the cache key is invalid
59-
*
6058
* @return ConfigInterface a ConfigInterface implementation containing the cached configuration data
59+
*
60+
* @throws InvalidArgumentException if the cache key is invalid
6161
*/
6262
public function __invoke(): ConfigInterface
6363
{

src/Helper/ConfigHelper.php

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
declare(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 <[email protected]>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
16+
namespace FastForward\Config\Helper;
17+
18+
use Dflydev\DotAccessData\Data;
19+
use Dflydev\DotAccessData\DataInterface;
20+
use 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+
*/
29+
final 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+
}

tests/AggregateConfigTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use FastForward\Config\AggregateConfig;
1919
use FastForward\Config\ArrayConfig;
2020
use FastForward\Config\ConfigInterface;
21+
use FastForward\Config\Helper\ConfigHelper;
2122
use PHPUnit\Framework\Attributes\CoversClass;
2223
use PHPUnit\Framework\Attributes\Test;
2324
use PHPUnit\Framework\Attributes\UsesClass;
@@ -29,6 +30,7 @@
2930
*/
3031
#[CoversClass(AggregateConfig::class)]
3132
#[UsesClass(ArrayConfig::class)]
33+
#[UsesClass(ConfigHelper::class)]
3234
final class AggregateConfigTest extends TestCase
3335
{
3436
use ProphecyTrait;

tests/ArrayConfigTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
use FastForward\Config\ArrayConfig;
1919
use FastForward\Config\Exception\InvalidArgumentException;
20+
use FastForward\Config\Helper\ConfigHelper;
2021
use PHPUnit\Framework\Attributes\CoversClass;
2122
use PHPUnit\Framework\Attributes\Test;
2223
use PHPUnit\Framework\Attributes\UsesClass;
@@ -26,6 +27,7 @@
2627
* @internal
2728
*/
2829
#[CoversClass(ArrayConfig::class)]
30+
#[UsesClass(ConfigHelper::class)]
2931
#[UsesClass(InvalidArgumentException::class)]
3032
final class ArrayConfigTest extends TestCase
3133
{

tests/CachedConfigTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,23 @@
22

33
declare(strict_types=1);
44

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 <[email protected]>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
516
namespace FastForward\Config\Tests;
617

718
use FastForward\Config\ArrayConfig;
819
use FastForward\Config\CachedConfig;
920
use FastForward\Config\ConfigInterface;
21+
use FastForward\Config\Helper\ConfigHelper;
1022
use PHPUnit\Framework\Attributes\CoversClass;
1123
use PHPUnit\Framework\Attributes\Test;
1224
use PHPUnit\Framework\Attributes\UsesClass;
@@ -15,8 +27,12 @@
1527
use Prophecy\Prophecy\ObjectProphecy;
1628
use Psr\SimpleCache\CacheInterface;
1729

30+
/**
31+
* @internal
32+
*/
1833
#[CoversClass(CachedConfig::class)]
1934
#[UsesClass(ArrayConfig::class)]
35+
#[UsesClass(ConfigHelper::class)]
2036
final class CachedConfigTest extends TestCase
2137
{
2238
use ProphecyTrait;

tests/Container/ConfigContainerTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use FastForward\Config\ConfigInterface;
2020
use FastForward\Config\Container\ConfigContainer;
2121
use FastForward\Config\Exception\ContainerNotFoundException;
22+
use FastForward\Config\Helper\ConfigHelper;
2223
use PHPUnit\Framework\Attributes\CoversClass;
2324
use PHPUnit\Framework\Attributes\Test;
2425
use PHPUnit\Framework\Attributes\UsesClass;
@@ -29,6 +30,7 @@
2930
*/
3031
#[CoversClass(ConfigContainer::class)]
3132
#[UsesClass(ArrayConfig::class)]
33+
#[UsesClass(ConfigHelper::class)]
3234
#[UsesClass(ContainerNotFoundException::class)]
3335
final class ConfigContainerTest extends TestCase
3436
{

tests/DirectoryConfigTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use FastForward\Config\ArrayConfig;
1919
use FastForward\Config\DirectoryConfig;
2020
use FastForward\Config\Exception\InvalidArgumentException;
21+
use FastForward\Config\Helper\ConfigHelper;
2122
use PHPUnit\Framework\Attributes\CoversClass;
2223
use PHPUnit\Framework\Attributes\Test;
2324
use PHPUnit\Framework\Attributes\UsesClass;
@@ -28,6 +29,7 @@
2829
*/
2930
#[CoversClass(DirectoryConfig::class)]
3031
#[UsesClass(ArrayConfig::class)]
32+
#[UsesClass(ConfigHelper::class)]
3133
#[UsesClass(InvalidArgumentException::class)]
3234
final class DirectoryConfigTest extends TestCase
3335
{

0 commit comments

Comments
 (0)