From 824833b3fecda2234d1b9a30b09ffb1dcb6948fe Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:39:57 +0600 Subject: [PATCH 01/15] Add config-aware merge semantics --- src/Config/ConfigMerge.php | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/Config/ConfigMerge.php diff --git a/src/Config/ConfigMerge.php b/src/Config/ConfigMerge.php new file mode 100644 index 0000000..49c8c9e --- /dev/null +++ b/src/Config/ConfigMerge.php @@ -0,0 +1,66 @@ + $base + * @param array $overlay + * @return array + */ + public static function merge(array $base, array $overlay): array + { + foreach ($overlay as $key => $value) { + if ( + array_key_exists($key, $base) + && self::isMap($base[$key]) + && self::isMap($value) + ) { + /** @var array $baseValue */ + $baseValue = $base[$key]; + /** @var array $overlayValue */ + $overlayValue = $value; + $base[$key] = self::merge($baseValue, $overlayValue); + + continue; + } + + $base[$key] = $value; + } + + return $base; + } + + /** + * @param iterable> $layers + * @return array + */ + public static function mergeMany(iterable $layers): array + { + $merged = []; + + foreach ($layers as $layer) { + if (!is_array($layer)) { + throw new InvalidArgumentException('Configuration merge layers must be arrays.'); + } + + $merged = self::merge($merged, $layer); + } + + return $merged; + } + + public static function isMap(mixed $value): bool + { + return is_array($value) && !array_is_list($value); + } +} From 97fcd8ae887e86a424c873fa7c3cbc1c3083de61 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:40:25 +0600 Subject: [PATCH 02/15] Include process-only values in environment enumeration --- src/Config/Support/Environment.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Config/Support/Environment.php b/src/Config/Support/Environment.php index bd9be98..302391c 100644 --- a/src/Config/Support/Environment.php +++ b/src/Config/Support/Environment.php @@ -21,8 +21,12 @@ public static function all(bool $includeHttpServerValues = false): array ); $env = array_filter($_ENV, is_string(...), ARRAY_FILTER_USE_KEY); + $process = getenv(); + $process = is_array($process) + ? array_filter($process, is_string(...), ARRAY_FILTER_USE_KEY) + : []; - return $env + $server; + return $env + $server + $process; } public static function get(?string $key = null, mixed $default = null): mixed From c666680c647cae6d33dd6af886e0b605e7cda04b Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:41:39 +0600 Subject: [PATCH 03/15] Treat corrupt lazy flat indexes as disposable cache misses --- .../Concerns/LazyFileConfigCacheTrait.php | 63 +++++++------------ 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/src/Config/Concerns/LazyFileConfigCacheTrait.php b/src/Config/Concerns/LazyFileConfigCacheTrait.php index 74cd239..c5cdd35 100644 --- a/src/Config/Concerns/LazyFileConfigCacheTrait.php +++ b/src/Config/Concerns/LazyFileConfigCacheTrait.php @@ -141,9 +141,7 @@ protected function collectFlatLeafIndex(string $namespace, array $namespaceData, } } - /** - * @return string[] - */ + /** @return string[] */ protected function discoverNamespaces(): array { $namespaces = []; @@ -222,20 +220,20 @@ protected function loadFlatLeafIndex(): void } $this->flatLeafIndex = []; - $path = $this->flatLeafIndexPath(); - if ($path === null || !is_file($path) || !is_readable($path)) { - $this->flatLeafIndexLoaded = true; - return; - } - - $loaded = include $path; - if (!is_array($loaded)) { - throw new UnexpectedValueException("Config file [{$path}] must return an array."); + if ($path !== null && is_file($path) && is_readable($path)) { + try { + $loaded = include $path; + if (is_array($loaded)) { + $this->flatLeafIndex = $this->filterFlatLeafIndex($loaded); + } + } catch (\Throwable) { + // Generated flat indexes are disposable acceleration artifacts. + // A corrupt index is a cache miss; namespace/source loading remains authoritative. + } } - $this->flatLeafIndex = $this->filterFlatLeafIndex($loaded); $this->flatLeafIndexLoaded = true; } @@ -267,8 +265,8 @@ protected function writeFlatLeafIndexFromCacheDirectory(): void } $index = $this->buildFlatLeafIndexFromDirectory($directory); - ksort($index); + if (!$this->writeCacheFile($indexPath, "flatLeafIndexLoaded = true; } - /** - * @param array $index - */ + /** @param array $index */ private function addFlatLeafIndexValue(array &$index, string $path, mixed $value): void { - if ( - $value === null - || is_bool($value) - || is_int($value) - || is_float($value) - || is_string($value) - ) { + if ($this->isCacheableLeafValue($value)) { $index[$path] = $value; } } - /** - * @return array - */ + /** @return array */ private function buildFlatLeafIndexFromDirectory(string $directory): array { $index = []; @@ -311,18 +299,20 @@ private function buildFlatLeafIndexFromDirectory(string $directory): array } $namespace = substr($entry, 0, -strlen($suffix)); - if ($namespace === '') { + if ($namespace === '' || preg_match('/^[A-Za-z0-9_-]+$/', $namespace) !== 1) { continue; } - if (!preg_match('/^[A-Za-z0-9_-]+$/', $namespace)) { + $path = $directory . DIRECTORY_SEPARATOR . $entry; + + try { + $loaded = include $path; + } catch (\Throwable) { continue; } - $path = $directory . DIRECTORY_SEPARATOR . $entry; - $loaded = include $path; if (!is_array($loaded)) { - throw new UnexpectedValueException("Config file [{$path}] must return an array."); + continue; } $this->collectFlatLeafIndex($namespace, $loaded, $index); @@ -363,11 +353,7 @@ private function flushAllNamespaceCacheFiles(): void $entries = scandir($directory); if ($entries !== false) { foreach ($entries as $entry) { - if ($entry === '.' || $entry === '..') { - continue; - } - - if (!$this->isOwnedNamespaceCacheEntry($entry)) { + if ($entry === '.' || $entry === '..' || !$this->isOwnedNamespaceCacheEntry($entry)) { continue; } @@ -406,9 +392,6 @@ private function isOwnedNamespaceCacheEntry(string $entry): bool return $namespace !== '' && preg_match('/^[A-Za-z0-9_-]+$/', $namespace) === 1; } - /** - * Hold one exclusive lock across namespace writes/deletes and flat-index rebuilding. - */ private function withNamespaceCacheLock(\Closure $operation): static { $directory = $this->namespaceCacheDirectory; From e2485d58fc19b14e358c8bc8555fc7b46f329b7f Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:42:24 +0600 Subject: [PATCH 04/15] Add resilient lazy namespace source fallback --- src/Config/ResilientLazyFileConfig.php | 68 ++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/Config/ResilientLazyFileConfig.php diff --git a/src/Config/ResilientLazyFileConfig.php b/src/Config/ResilientLazyFileConfig.php new file mode 100644 index 0000000..197114b --- /dev/null +++ b/src/Config/ResilientLazyFileConfig.php @@ -0,0 +1,68 @@ +loadedNamespaces[$namespace])) { + return; + } + + $loaded = $this->loadCachedNamespace($namespace); + if ($loaded === null) { + $source = $this->resolveNamespaceFile($namespace); + if ($source === null) { + $this->loadedNamespaces[$namespace] = true; + + return; + } + + $loaded = include $source; + if (!is_array($loaded)) { + throw new UnexpectedValueException("Config file [{$source}] must return an array."); + } + } + + $this->loadedNamespaces[$namespace] = true; + + if (!array_key_exists($namespace, $this->items)) { + $this->items[$namespace] = $loaded; + + return; + } + + if (is_array($this->items[$namespace])) { + $this->items[$namespace] = array_replace_recursive($loaded, $this->items[$namespace]); + } + } + + /** + * @return array|null + */ + private function loadCachedNamespace(string $namespace): ?array + { + $cache = $this->resolveCachedNamespaceFile($namespace); + if ($cache === null) { + return null; + } + + try { + $loaded = include $cache; + } catch (\Throwable) { + return null; + } + + return is_array($loaded) ? $loaded : null; + } +} From 62433596aa40c15929c45039883d9886dc7e9c28 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:42:45 +0600 Subject: [PATCH 05/15] Add layered lazy configuration primitive --- src/Config/LayeredLazyFileConfig.php | 158 +++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/Config/LayeredLazyFileConfig.php diff --git a/src/Config/LayeredLazyFileConfig.php b/src/Config/LayeredLazyFileConfig.php new file mode 100644 index 0000000..3aab337 --- /dev/null +++ b/src/Config/LayeredLazyFileConfig.php @@ -0,0 +1,158 @@ + */ + private array $fallback; + + /** @var array */ + private array $knownNamespaces = []; + + /** @var array */ + private array $materializedNamespaces = []; + + /** @var array */ + private array $overrides; + + private ResilientLazyFileConfig $source; + + /** + * @param array $fallback + * @param array $overrides + * @param list $namespaces + */ + public function __construct( + string $directory, + ?string $namespaceCacheDirectory = null, + array $fallback = [], + array $overrides = [], + array $namespaces = [], + string $extension = 'php', + ) { + $this->source = new ResilientLazyFileConfig( + directory: $directory, + extension: $extension, + namespaceCacheDirectory: $namespaceCacheDirectory, + ); + $this->fallback = $fallback; + $this->overrides = $overrides; + + foreach ([...array_keys($fallback), ...array_keys($overrides), ...$namespaces] as $namespace) { + if (is_string($namespace) && $namespace !== '') { + $this->knownNamespaces[$namespace] = true; + } + } + } + + /** @return array */ + #[\Override] + public function all(): array + { + foreach (array_keys($this->knownNamespaces) as $namespace) { + $this->materializeNamespace($namespace); + } + + $items = []; + foreach (parent::all() as $key => $value) { + if (is_string($key)) { + $items[$key] = $value; + } + } + + return $items; + } + + public function clearNamespaceCache(): static + { + $this->source->flushNamespaceCache(); + + return $this; + } + + public function namespaceCacheDirectory(): ?string + { + return $this->source->namespaceCacheDirectory(); + } + + /** + * @param string|array|null $namespaces + */ + public function warmNamespaceCache(string|array|null $namespaces = null): static + { + $this->source->warmNamespaceCache($namespaces ?? array_keys($this->knownNamespaces)); + + return $this; + } + + #[\Override] + protected function resolveRawValue(int|string $key): mixed + { + if (!is_string($key) || $key === '') { + return parent::resolveRawValue($key); + } + + $namespace = $this->namespaceFromPath($key); + $this->materializeNamespace($namespace); + + return parent::resolveRawValue($key); + } + + private function materializeNamespace(string $namespace): void + { + if (isset($this->materializedNamespaces[$namespace])) { + return; + } + + $missing = $this->missingValueMarker(); + $source = $this->source->get($namespace, $missing); + $layers = []; + + if (array_key_exists($namespace, $this->fallback)) { + $layers[] = [$namespace => $this->fallback[$namespace]]; + } + + if ($source !== $missing) { + $layers[] = [$namespace => $source]; + } + + if (array_key_exists($namespace, $this->overrides)) { + $layers[] = [$namespace => $this->overrides[$namespace]]; + } + + if ($layers !== []) { + $merged = ConfigMerge::mergeMany($layers); + if (array_key_exists($namespace, $merged)) { + $this->items[$namespace] = $merged[$namespace]; + } + } + + $this->knownNamespaces[$namespace] = true; + $this->materializedNamespaces[$namespace] = true; + $this->flushReadCache(); + } + + private function namespaceFromPath(string $path): string + { + $dot = strpos($path, '.'); + $namespace = $dot === false ? $path : substr($path, 0, $dot); + $namespace = trim($namespace); + + if ($namespace === '') { + return $path; + } + + return $namespace; + } +} From 6602f0d190370d885dc24fad79c652b399b802ab Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:43:31 +0600 Subject: [PATCH 06/15] Add configuration layering and resilient cache regression tests --- tests/Feature/ConfigLayeringTest.php | 181 +++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 tests/Feature/ConfigLayeringTest.php diff --git a/tests/Feature/ConfigLayeringTest.php b/tests/Feature/ConfigLayeringTest.php new file mode 100644 index 0000000..056a5da --- /dev/null +++ b/tests/Feature/ConfigLayeringTest.php @@ -0,0 +1,181 @@ +layerConfigPath = sys_get_temp_dir() . '/arraykit-layer-source-' . bin2hex(random_bytes(5)); + $this->layerCachePath = sys_get_temp_dir() . '/arraykit-layer-cache-' . bin2hex(random_bytes(5)); + mkdir($this->layerConfigPath, 0777, true); + mkdir($this->layerCachePath, 0777, true); +}); + +afterEach(function () { + configLayeringRemove($this->layerConfigPath); + configLayeringRemove($this->layerCachePath); +}); + +it('recursively merges maps while replacing lists atomically', function () { + $merged = ConfigMerge::merge( + [ + 'app' => ['name' => 'ArrayKit', 'debug' => false], + 'middleware' => ['auth', 'csrf'], + 'cache' => ['driver' => 'file'], + ], + [ + 'app' => ['debug' => true], + 'middleware' => ['api'], + 'cache' => false, + ], + ); + + expect($merged)->toBe([ + 'app' => ['name' => 'ArrayKit', 'debug' => true], + 'middleware' => ['api'], + 'cache' => false, + ]); +}); + +it('merges many configuration layers in order', function () { + expect(ConfigMerge::mergeMany([ + ['app' => ['name' => 'base'], 'hosts' => ['a', 'b']], + ['app' => ['debug' => false]], + ['app' => ['name' => 'override'], 'hosts' => ['x']], + ]))->toBe([ + 'app' => ['name' => 'override', 'debug' => false], + 'hosts' => ['x'], + ]); +}); + +it('keeps exact layered reads equivalent to fully merged namespace reads', function () { + configLayeringWrite($this->layerConfigPath, 'app', [ + 'servers' => ['a', 'b'], + 'cache' => ['driver' => 'redis'], + 'nested' => ['source' => true], + ]); + + $config = new LayeredLazyFileConfig( + directory: $this->layerConfigPath, + fallback: [ + 'app' => [ + 'fallback' => true, + 'nested' => ['fallback' => true], + ], + ], + overrides: [ + 'app' => [ + 'servers' => ['x'], + 'cache' => false, + 'nested' => ['override' => true], + ], + ], + namespaces: ['app'], + ); + + expect($config->get('app.servers'))->toBe(['x']) + ->and($config->get('app.servers.1', 'missing'))->toBe('missing') + ->and($config->get('app.cache.driver', 'missing'))->toBe('missing') + ->and($config->get('app.fallback'))->toBeTrue() + ->and($config->get('app.nested'))->toBe([ + 'fallback' => true, + 'source' => true, + 'override' => true, + ]); +}); + +it('materializes layered namespaces with fallback source override precedence', function () { + configLayeringWrite($this->layerConfigPath, 'db', [ + 'host' => 'source', + 'options' => ['timeout' => 5], + ]); + + $config = new LayeredLazyFileConfig( + directory: $this->layerConfigPath, + fallback: ['db' => ['host' => 'fallback', 'port' => 3306]], + overrides: ['db' => ['host' => 'override']], + namespaces: ['db'], + ); + + expect($config->all())->toBe([ + 'db' => [ + 'host' => 'override', + 'port' => 3306, + 'options' => ['timeout' => 5], + ], + ]); +}); + +it('falls back to source when a generated namespace cache returns invalid data', function () { + configLayeringWrite($this->layerConfigPath, 'db', ['host' => 'source']); + file_put_contents($this->layerCachePath . '/db.php', "layerConfigPath, + namespaceCacheDirectory: $this->layerCachePath, + ); + + expect($config->get('db.host'))->toBe('source'); +}); + +it('falls back to source when generated cache php is malformed', function () { + configLayeringWrite($this->layerConfigPath, 'db', ['host' => 'source']); + file_put_contents($this->layerCachePath . '/db.php', "layerCachePath . '/__flat.php', "layerConfigPath, + namespaceCacheDirectory: $this->layerCachePath, + ); + + expect($config->get('db.host'))->toBe('source'); +}); + +it('does not hide invalid source configuration behind resilient cache behavior', function () { + file_put_contents($this->layerConfigPath . '/db.php', "layerCachePath . '/db.php', "layerConfigPath, + namespaceCacheDirectory: $this->layerCachePath, + ); + + expect(fn () => $config->get('db.host'))->toThrow(UnexpectedValueException::class); +}); From b393558f0ac94919ba7340b478f134fdf8b89147 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:43:47 +0600 Subject: [PATCH 07/15] Cover process environment enumeration and precedence --- tests/Feature/EnvironmentSupportTest.php | 77 ++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/Feature/EnvironmentSupportTest.php diff --git a/tests/Feature/EnvironmentSupportTest.php b/tests/Feature/EnvironmentSupportTest.php new file mode 100644 index 0000000..0f2d8b0 --- /dev/null +++ b/tests/Feature/EnvironmentSupportTest.php @@ -0,0 +1,77 @@ +toBe('process-value') + ->and(Environment::get($key))->toBe('process-value') + ->and(Environment::has($key))->toBeTrue(); + } finally { + if ($envExists) { + $_ENV[$key] = $envValue; + } else { + unset($_ENV[$key]); + } + + if ($serverExists) { + $_SERVER[$key] = $serverValue; + } else { + unset($_SERVER[$key]); + } + + putenv($previous === false ? $key : $key . '=' . $previous); + } +}); + +it('keeps environment source precedence consistent across get and all', function () { + $key = 'ARRAYKIT_ENV_PRECEDENCE_' . strtoupper(bin2hex(random_bytes(4))); + $previous = getenv($key); + $envExists = array_key_exists($key, $_ENV); + $envValue = $_ENV[$key] ?? null; + $serverExists = array_key_exists($key, $_SERVER); + $serverValue = $_SERVER[$key] ?? null; + + putenv($key . '=process'); + $_SERVER[$key] = 'server'; + $_ENV[$key] = 'env'; + + try { + expect(Environment::get($key))->toBe('env') + ->and(Environment::all()[$key] ?? null)->toBe('env'); + + unset($_ENV[$key]); + expect(Environment::get($key))->toBe('server') + ->and(Environment::all()[$key] ?? null)->toBe('server'); + + unset($_SERVER[$key]); + expect(Environment::get($key))->toBe('process') + ->and(Environment::all()[$key] ?? null)->toBe('process'); + } finally { + if ($envExists) { + $_ENV[$key] = $envValue; + } else { + unset($_ENV[$key]); + } + + if ($serverExists) { + $_SERVER[$key] = $serverValue; + } else { + unset($_SERVER[$key]); + } + + putenv($previous === false ? $key : $key . '=' . $previous); + } +}); From 60401bb46bc236ec01f6ae53fc0da01a49519fb4 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:44:20 +0600 Subject: [PATCH 08/15] Document configuration layering and resilient cache behavior --- docs/config-layering.rst | 94 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/config-layering.rst diff --git a/docs/config-layering.rst b/docs/config-layering.rst new file mode 100644 index 0000000..941742e --- /dev/null +++ b/docs/config-layering.rst @@ -0,0 +1,94 @@ +Configuration Layering +====================== + +ArrayKit provides explicit configuration-layer semantics for applications that +compose defaults, file-backed configuration, and runtime overrides. + +ConfigMerge +----------- + +``ConfigMerge`` recursively merges associative configuration maps while treating +lists as atomic values. A higher-precedence list replaces the lower list instead +of merging numeric indexes. + +.. code-block:: php + + ['name' => 'Example', 'debug' => false], + 'middleware' => ['auth', 'csrf'], + ], + [ + 'app' => ['debug' => true], + 'middleware' => ['api'], + ], + ); + + // middleware is ['api'], not ['api', 'csrf']. + +Use ``ConfigMerge::mergeMany()`` when composing multiple layers in precedence +order. + +LayeredLazyFileConfig +--------------------- + +``LayeredLazyFileConfig`` composes three layers with this precedence: + +``fallback < lazy source < overrides`` + +Only the requested namespace is materialized. Exact path reads are resolved from +the fully merged namespace, so list replacement and scalar shadowing cannot leak +values from lower-precedence layers. + +.. code-block:: php + + ['debug' => false], + ], + overrides: [ + 'app' => ['debug' => true], + ], + namespaces: ['app', 'cache', 'database'], + ); + + $debug = $config->get('app.debug'); + +``all()`` materializes the configured namespace set. ``warmNamespaceCache()`` +and ``clearNamespaceCache()`` delegate generated source-cache lifecycle to the +underlying lazy source. + +Resilient Lazy Source Cache +--------------------------- + +``ResilientLazyFileConfig`` treats generated namespace-cache corruption as a +cache miss and retries the authoritative source namespace. Invalid source files +still fail normally; resilience applies only to disposable generated cache +artifacts. + +Malformed or invalid ``__flat.php`` indexes are also treated as cache misses. +This keeps an acceleration artifact from preventing source configuration from +loading. + +Environment Enumeration +----------------------- + +``Environment::all()`` enumerates all three runtime sources with the same +precedence used by ``Environment::get()``: + +1. ``$_ENV``; +2. non-HTTP ``$_SERVER`` values; +3. process values returned by ``getenv()``. + +This means process-only variables are visible during complete environment +enumeration as well as direct lookup. From cc0291588e406bddd9868432d80c73c807a6f21b Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:44:27 +0600 Subject: [PATCH 09/15] Add configuration layering guide to manual --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.rst b/docs/index.rst index 0dd81ce..f99499e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,6 +25,7 @@ Contents collection config lazy-config + config-layering traits-and-helpers migration rule-reference From 2a878400a905f1596385feed44ed8d88015a168d Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:49:47 +0600 Subject: [PATCH 10/15] refactor --- src/Config/ConfigMerge.php | 10 +++++----- src/Config/LayeredLazyFileConfig.php | 16 ++++------------ 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Config/ConfigMerge.php b/src/Config/ConfigMerge.php index 49c8c9e..e4d78fe 100644 --- a/src/Config/ConfigMerge.php +++ b/src/Config/ConfigMerge.php @@ -10,6 +10,11 @@ final class ConfigMerge { private function __construct() {} + public static function isMap(mixed $value): bool + { + return is_array($value) && !array_is_list($value); + } + /** * Recursively overlay configuration maps while replacing list values atomically. * @@ -58,9 +63,4 @@ public static function mergeMany(iterable $layers): array return $merged; } - - public static function isMap(mixed $value): bool - { - return is_array($value) && !array_is_list($value); - } } diff --git a/src/Config/LayeredLazyFileConfig.php b/src/Config/LayeredLazyFileConfig.php index 3aab337..406bd02 100644 --- a/src/Config/LayeredLazyFileConfig.php +++ b/src/Config/LayeredLazyFileConfig.php @@ -14,8 +14,7 @@ */ class LayeredLazyFileConfig extends Config { - /** @var array */ - private array $fallback; + private readonly ResilientLazyFileConfig $source; /** @var array */ private array $knownNamespaces = []; @@ -23,11 +22,6 @@ class LayeredLazyFileConfig extends Config /** @var array */ private array $materializedNamespaces = []; - /** @var array */ - private array $overrides; - - private ResilientLazyFileConfig $source; - /** * @param array $fallback * @param array $overrides @@ -36,8 +30,8 @@ class LayeredLazyFileConfig extends Config public function __construct( string $directory, ?string $namespaceCacheDirectory = null, - array $fallback = [], - array $overrides = [], + private array $fallback = [], + private array $overrides = [], array $namespaces = [], string $extension = 'php', ) { @@ -46,10 +40,8 @@ public function __construct( extension: $extension, namespaceCacheDirectory: $namespaceCacheDirectory, ); - $this->fallback = $fallback; - $this->overrides = $overrides; - foreach ([...array_keys($fallback), ...array_keys($overrides), ...$namespaces] as $namespace) { + foreach ([...array_keys($this->fallback), ...array_keys($this->overrides), ...$namespaces] as $namespace) { if (is_string($namespace) && $namespace !== '') { $this->knownNamespaces[$namespace] = true; } From df9b658400ab2b73e7e55d84f3ce14cbf4d4e318 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:52:23 +0600 Subject: [PATCH 11/15] Fix lazy cache index by-ref type --- src/Config/Concerns/LazyFileConfigCacheTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Concerns/LazyFileConfigCacheTrait.php b/src/Config/Concerns/LazyFileConfigCacheTrait.php index c5cdd35..c414771 100644 --- a/src/Config/Concerns/LazyFileConfigCacheTrait.php +++ b/src/Config/Concerns/LazyFileConfigCacheTrait.php @@ -275,7 +275,7 @@ protected function writeFlatLeafIndexFromCacheDirectory(): void $this->flatLeafIndexLoaded = true; } - /** @param array $index */ + /** @param array $index */ private function addFlatLeafIndexValue(array &$index, string $path, mixed $value): void { if ($this->isCacheableLeafValue($value)) { From de343ab3b30a6fd8cc960fea8d67a1330d33d69b Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:52:34 +0600 Subject: [PATCH 12/15] Remove redundant merge layer type guard --- src/Config/ConfigMerge.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Config/ConfigMerge.php b/src/Config/ConfigMerge.php index e4d78fe..c2cef99 100644 --- a/src/Config/ConfigMerge.php +++ b/src/Config/ConfigMerge.php @@ -4,8 +4,6 @@ namespace Infocyph\ArrayKit\Config; -use InvalidArgumentException; - final class ConfigMerge { private function __construct() {} @@ -54,10 +52,6 @@ public static function mergeMany(iterable $layers): array $merged = []; foreach ($layers as $layer) { - if (!is_array($layer)) { - throw new InvalidArgumentException('Configuration merge layers must be arrays.'); - } - $merged = self::merge($merged, $layer); } From 5d9ca2948819cdda640e971dcda9b8c9af4033b7 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:52:51 +0600 Subject: [PATCH 13/15] Remove redundant namespace string check --- src/Config/LayeredLazyFileConfig.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/LayeredLazyFileConfig.php b/src/Config/LayeredLazyFileConfig.php index 406bd02..6cc294a 100644 --- a/src/Config/LayeredLazyFileConfig.php +++ b/src/Config/LayeredLazyFileConfig.php @@ -42,7 +42,7 @@ public function __construct( ); foreach ([...array_keys($this->fallback), ...array_keys($this->overrides), ...$namespaces] as $namespace) { - if (is_string($namespace) && $namespace !== '') { + if ($namespace !== '') { $this->knownNamespaces[$namespace] = true; } } From a795434b436f257a923e67d4eba9331af94bedd9 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:53:04 +0600 Subject: [PATCH 14/15] Remove redundant process environment array check --- src/Config/Support/Environment.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Config/Support/Environment.php b/src/Config/Support/Environment.php index 302391c..0e325bb 100644 --- a/src/Config/Support/Environment.php +++ b/src/Config/Support/Environment.php @@ -22,9 +22,6 @@ public static function all(bool $includeHttpServerValues = false): array $env = array_filter($_ENV, is_string(...), ARRAY_FILTER_USE_KEY); $process = getenv(); - $process = is_array($process) - ? array_filter($process, is_string(...), ARRAY_FILTER_USE_KEY) - : []; return $env + $server + $process; } From d4f73cb501ca894caaad56a7e15cebb1ec64b51c Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 6 Sep 2026 13:56:23 +0600 Subject: [PATCH 15/15] Tighten lazy flat-index static types --- src/Config/Concerns/LazyFileConfigCacheTrait.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Config/Concerns/LazyFileConfigCacheTrait.php b/src/Config/Concerns/LazyFileConfigCacheTrait.php index c414771..38e9374 100644 --- a/src/Config/Concerns/LazyFileConfigCacheTrait.php +++ b/src/Config/Concerns/LazyFileConfigCacheTrait.php @@ -275,10 +275,16 @@ protected function writeFlatLeafIndexFromCacheDirectory(): void $this->flatLeafIndexLoaded = true; } - /** @param array $index */ + /** @param array $index */ private function addFlatLeafIndexValue(array &$index, string $path, mixed $value): void { - if ($this->isCacheableLeafValue($value)) { + if ( + $value === null + || is_bool($value) + || is_int($value) + || is_float($value) + || is_string($value) + ) { $index[$path] = $value; } } @@ -286,6 +292,7 @@ private function addFlatLeafIndexValue(array &$index, string $path, mixed $value /** @return array */ private function buildFlatLeafIndexFromDirectory(string $directory): array { + /** @var array $index */ $index = []; $entries = scandir($directory); if ($entries === false) { @@ -327,6 +334,7 @@ private function buildFlatLeafIndexFromDirectory(string $directory): array */ private function filterFlatLeafIndex(array $loaded): array { + /** @var array $index */ $index = []; foreach ($loaded as $key => $value) {