Deep Merging Arrays in PHP with array_replace_recursive
The Problem with Simple Merging
When I first started building a multi‑module web application, each component shipped its own default settings. I tried to combine them using the classic array_merge function, only to discover that nested arrays were silently overwritten. Imagine a module that defines a database connection array like ['host' => 'localhost', 'options' => ['port' => 3306]] and another module that adds caching with ['cache' => ['driver' => 'redis']]. A simple merge left the options subtree untouched, but if the second module also wanted to add a port under options, the entire options array would be replaced, breaking the connection configuration.
The root cause is that array_merge treats sub‑arrays as atomic values; it does not descend into them. In a production environment where defaults, environment‑specific overrides, and user‑provided settings must coexist, we need a way to blend these structures without losing depth.
Introducing array_replace_recursive
PHP ships a built‑in function that does exactly what we need: array_replace_recursive. Unlike array_merge, it walks the array tree recursively, merging matching keys while preserving non‑overlapping branches. The signature is simple:
array_replace_recursive(array $array1, array $array2, array ...$others): array
The first argument is the base configuration; subsequent arguments are the overrides. If a key exists in both arrays and both values are arrays, the function recurses. Otherwise, the value from the later argument wins. This behavior matches the mental model of "deep merge" that developers often expect from configuration libraries in other languages.
Internally, the function uses array_replace as a building block but adds a recursive step. Because it’s part of the core PHP extension, there’s no extra dependency to manage, and the performance impact is negligible for typical configuration sizes.
Real‑World Example: Configuration Management
Consider a SaaS platform where services such as email, payments, and logging each define their own configuration defaults. At startup we load these defaults and then apply environment‑specific overrides and finally user‑provided settings. Using array_replace_recursive keeps the hierarchy intact.
Tip: Always apply overrides in order of increasing specificity—defaults first, then environment variables, then runtime options.
Here is a minimal reproduction that you can drop into a project:
[
'driver' => 'smtp',
'host' => 'smtp.default.com',
'options' => [
'port' => 587,
'encryption' => 'tls',
],
],
'payment' => [
'gateway' => 'paypal',
'api_key' => 'default_key',
],
'logging' => [
'handler' => 'file',
'path' => '/var/log/app.log',
],
];
// Environment‑specific overrides (e.g., production)
services_env = [
'email' => [
'host' => 'smtp.prod.com',
'options' => [
'port' => 465,
],
],
'logging' => [
'handler' => 'syslog',
],
];
// Runtime user options (e.g., from a web form)
services_user = [
'payment' => [
'api_key' => 'user_provided_key',
'webhook_secret' => 'xyz123',
],
'logging' => [
'level' => 'debug',
],
];
// Deep merge – order matters
services_final = array_replace_recursive(
$services_default,
$services_env,
$services_user,
);
// Output the result (for demonstration)
print_r($services_final);
?>
The resulting array preserves the original defaults for keys that were not overridden, while allowing nested values to be updated. Notice how the options subtree under email retains the encryption setting (from defaults) even though port was replaced by the environment override.
Best Practices and Pitfalls
While array_replace_recursive is powerful, a few nuances deserve attention:
- Key order. The function does not guarantee any particular ordering of merged keys, which can affect serialization (e.g., JSON). If order matters, consider sorting after the merge or using a library that respects insertion order (PHP 8.1+ arrays preserve order).
- Performance. The recursive nature means each level is traversed. For extremely deep or large configurations, the overhead is still negligible, but you might want to cache the merged result across requests.
- Non‑array values. If a key maps to a non‑array in the base but an array in the override (or vice‑versa), the non‑array wins. This can be surprising, so document the expected shape of each configuration block.
- Numeric keys. When numeric keys are involved, the function treats them as strings, which may lead to unexpected merging. If you need to merge indexed arrays, consider converting them to associative arrays first or using a dedicated data‑structure library.
One common anti‑pattern is to rely on array_replace_recursive for merging unrelated data structures, such as merging two request payloads. In those cases, a shallow merge is appropriate, and using array_merge (or even a simple assignment) is clearer and faster.
Wrapping Up
Deep merging arrays is a routine task in PHP applications that handle configuration, API payloads, or hierarchical data. array_replace_recursive gives you a concise, idiomatic way to blend nested structures without losing depth, and it’s built right into the language. By applying overrides in order of specificity and being mindful of its behavior with non‑array values and numeric keys, you can keep your codebase clean and maintainable. Next time you find yourself manually traversing arrays, remember that PHP already provides a recursive solution—just let array_replace_recursive do the heavy lifting.