Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,39 @@ public function getSectionNames():array {
return $names;
}

public function merge(Config $configToOverride):void {
/**
* Merge another Config into this instance immutably, returning a new Config.
* Existing values are preserved; only missing keys/sections are filled from the overriding config.
*/
public function withMerge(Config $configToOverride): self {
// Start with current sections
$mergedSectionList = $this->sectionList;

foreach($configToOverride->getSectionNames() as $sectionName) {
if(isset($this->sectionList[$sectionName])) {
foreach($configToOverride->getSection($sectionName)
as $key => $value) {
if(empty($this->sectionList[$sectionName][$key])) {
$this->sectionList[$sectionName] = $this->sectionList[$sectionName]->with($key, $value);
$overrideSection = $configToOverride->getSection($sectionName);
if(isset($mergedSectionList[$sectionName])) {
// Fill only missing keys from override into existing section
foreach($overrideSection as $key => $value) {
if(empty($mergedSectionList[$sectionName][$key])) {
$mergedSectionList[$sectionName] = $mergedSectionList[$sectionName]->with($key, $value);
}
}
}
else {
$this->sectionList[$sectionName] = $configToOverride->getSection(
$sectionName
);
// Add whole section if it doesn't exist
$mergedSectionList[$sectionName] = $overrideSection;
}
}

return new self(...array_values($mergedSectionList));
}

/**
* @deprecated Use withMerge() for an immutable alternative. This method will be removed in a future major release.
*/
public function merge(Config $configToOverride):void {
@trigger_error("Config::merge() is deprecated. Use Config::withMerge() for an immutable merge.", E_USER_DEPRECATED);
$new = $this->withMerge($configToOverride);
$this->sectionList = $new->sectionList;
}
}
}
2 changes: 1 addition & 1 deletion src/ConfigFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static function createForProject(
}

if($previousConfig) {
$config->merge($previousConfig);
$config = $config->withMerge($previousConfig);
}

$previousConfig = $config;
Expand Down
67 changes: 67 additions & 0 deletions test/phpunit/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,71 @@ public function testTypeSafeGetter() {
self::assertEquals("April 5th 1988", $dateTime->format("F jS Y"));
self::assertNull($sut->getFloat("nothing-here"));
}

public function testWithMergeReturnsNewAndDoesNotMutateOriginal():void {
$original = new Config(
new ConfigSection("app", [
"namespace" => "ExampleAppOriginal",
]),
new ConfigSection("db", [
"host" => "localhost",
])
);

$override = new Config(
new ConfigSection("app", [
"namespace" => "Override",
"extra" => "value",
]),
new ConfigSection("cache", [
"enabled" => "1",
])
);

$merged = $original->withMerge($override);

// Ensure new instance returned
self::assertNotSame($original, $merged);

// Original remains unchanged (use section access to avoid env var interference)
self::assertSame("ExampleAppOriginal", $original->getSection("app")->get("namespace"));
self::assertNull($original->getSection("app")->get("extra"));
self::assertNull($original->getSection("cache"));

// Merged config has expected values
self::assertSame("ExampleAppOriginal", $merged->getSection("app")->get("namespace")); // existing preserved
self::assertSame("value", $merged->getSection("app")->get("extra")); // new key added
self::assertSame("1", $merged->getSection("cache")->get("enabled")); // new section added
}

public function testMergeEmitsDeprecationAndMutates():void {
$original = new Config(
new ConfigSection("app", [
"namespace" => "ExampleAppOriginal",
])
);
$override = new Config(
new ConfigSection("app", [
"extra" => "value",
])
);

$deprecationCount = 0;
set_error_handler(function(int $errno, string $errstr) use (&$deprecationCount) {
if($errno === E_USER_DEPRECATED) {
$deprecationCount++;
}
return true; // prevent PHPUnit from handling
});

try {
$original->merge($override);
}
finally {
restore_error_handler();
}

self::assertSame(1, $deprecationCount);
self::assertSame("value", $original->getSection("app")->get("extra")); // mutated
}
}