Simplifying Complex Array Merging in PHP with Recursive Merging
I’ve lost count of how many times I’ve had to merge configuration arrays in PHP projects—especially when dealing with default settings overridden by environment-specific values, user preferences, or plugin extensions. At first glance, it seems simple: just use array_merge(). But when those arrays are nested—think multi-level config trees—it quickly becomes a mess. The top-level keys merge fine, but deeper levels get overwritten entirely instead of being intelligently combined. That’s where a recursive merge strategy saves the day.
Here’s a real-world example: imagine you’re building a CMS where each module can define its own default configuration, but site administrators can override those values in a global config file. You want admin settings to win, but only for the specific keys they’ve defined—leaving the rest of the module’s defaults intact.
/**
* Recursively merges two or more arrays, preserving nested structure.
* Later arrays override earlier ones, but only at matching keys.
* If both values are arrays, they are merged recursively.
*
* @param array ...$arrays Arrays to merge (left to right, right wins)
* @return array
*/
function array_merge_recursive_distinct(array ...$arrays): array
{
$result = [];
foreach ($arrays as $array) {
if (!is_array($array)) {
continue; // Skip non-arrays safely
}
foreach ($array as $key => $value) {
if (is_int($key)) {
// Numeric keys: append (like array_merge)
$result[] = $value;
} elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
// Both are arrays: merge recursively
$result[$key] = array_merge_recursive_distinct($result[$key], $value);
} else {
// Override or set new value
$result[$key] = $value;
}
}
}
return $result;
}
This function behaves like a smarter version of PHP’s built-in array_merge_recursive(), which has a frustrating quirk: when both arrays have the same string key, it combines their values into a new array instead of letting the later one override. That’s rarely what you want in config merging.
With this approach, you get predictable, layered overrides—exactly how you’d expect configuration to work. Want to test it? Here’s how it plays out:
$defaultConfig = [
'database' => [
'host' => 'localhost',
'port' => 3306,
'charset' => 'utf8mb4'
],
'cache' => [
'enabled' => true,
'ttl' => 3600
],
'debug' => false
];
$overrideConfig = [
'database' => [
'host' => 'db.production.internal',
'port' => 5432 // Note: only overriding host and port
],
'debug' => true
];
$finalConfig = array_merge_recursive_distinct($defaultConfig, $overrideConfig);
/* Result:
* [
* 'database' => [
* 'host' => 'db.production.internal',
* 'port' => 5432,
* 'charset' => 'utf8mb4' // preserved from default
* ],
* 'cache' => [
* 'enabled' => true,
* 'ttl' => 3600 // untouched
* ],
* 'debug' => true
* ]
*/
Notice how charset under database wasn’t lost, even though we didn’t mention it in the override? That’s the power of recursion—it respects the structure.
Why not just use a library or array_replace_recursive()? Well, array_replace_recursive() does get us close, but it doesn’t handle numeric keys the way we often need—it replaces them instead of appending. And while there are solid Composer packages for deep merging, sometimes you want a zero-dependency, transparent solution you can drop into a helper file and trust.
I’ve used variations of this in Drupal plugins, Laravel service providers, and even standalone CLI tools. It’s one of those small utilities that pays for itself the second you stop fighting config merges and start focusing on actual features.
Tip: If you’re working with configuration that might come from JSON or YAML files, wrap this in a loader function that reads and merges files in priority order—base, environment, local overrides. It makes deployment and local development infinitely smoother.
The next time you’re tempted to write a nested foreach loop to merge settings, pause. Ask yourself: do I really want to reinvent this wheel? Chances are, a clean recursive merge function like this one will do the job better, faster, and with fewer bugs.