Merging Configurations

Applications often assemble their configuration from several layers: built-in defaults, a configuration file shared by the team and a local file of each developer. Each layer overrides only the items it mentions. This page shows:

  • how processMultiple() merges the layers and validates the result
  • how structures, arrays and lists are combined
  • how to choose another strategy with mergeMode() or your own function
  • how arrays without described items are merged

The examples continue the configuration of an application from the overview. $processor is an instance of Nette\Schema\Processor.

Merging Layers: processMultiple()

The method processMultiple() takes several datasets, merges them in order so that later ones take precedence, and validates the final result as a whole:

$schema = Expect::structure([
	'host' => Expect::string(),
	'port' => Expect::int(),
	'logging' => Expect::bool(),
]);

$defaults = ['host' => 'localhost', 'port' => 3306, 'logging' => false];
$local = ['port' => 5432, 'logging' => true];

$config = $processor->processMultiple($schema, [$defaults, $local]);
// $config = {'host' => 'localhost', 'port' => 5432, 'logging' => true}

The host item keeps its default value because the local file did not set it, while port and logging are overwritten by the later dataset.

Notice that the result is validated only once, after merging. A layer on its own may be incomplete, for example the defaults usually lack the password, and only the merged configuration must contain everything that is required.

How Values Are Combined

The schema decides how two layers are combined:

  • a simple value, such as a string or a number, is replaced by the later one
  • a structure and an array with string keys are merged by keys, recursively according to the schemas of their items
  • the items of a list (listOf(), or arrayOf() with numeric keys) are appended one after another
  • a tuple is replaced as a whole, because mixing the positions of two tuples would only produce nonsense

So with 'admins' => Expect::listOf('email'), the administrators from the local file are added to those from the shared one:

$schema = Expect::structure([
	'admins' => Expect::listOf('email'),
]);

$config = $processor->processMultiple($schema, [
	['admins' => ['jim@example.com']],
	['admins' => ['ann@example.com']],
]);
// $config->admins = ['jim@example.com', 'ann@example.com']

The default value of an array is merged with the layers as well: its items stay unless a layer overrides them, and the items of the layers are appended to a list. To turn this off, call mergeDefaults(false).

Choosing a Strategy: mergeMode()

When appending or merging by keys is not what you want, arrays and structures can choose another strategy with mergeMode(). Let's say the local file should replace the list of administrators instead of adding to it:

use Nette\Schema\MergeMode;

Expect::listOf('email')->mergeMode(MergeMode::Replace); // the later list replaces the earlier one

MergeMode::AppendKeys merges by keys and appends list items, OverwriteKeys also merges by keys but overwrites list items by their position, and Replace keeps only the later value.

In a NEON configuration file of a Nette application, an exclamation mark after a key, admins!:, prevents the merging of that one item, see configuration of the DI container.

Arrays Without Described Items

An array whose items are not described, such as Expect::array(), is merged deeply, key by key at every level:

$schema = Expect::structure([
	'options' => Expect::array(),
]);

$processor->processMultiple($schema, [
	['options' => ['cache' => ['ttl' => 60]]],
	['options' => ['cache' => ['ttl' => 3600]]],
]);
// OK, returns {'options' => ['cache' => ['ttl' => 3600]]}

Keep in mind that the schema then cannot check what the arrays contain. To let the later layer replace the array as a whole, choose mergeMode(MergeMode::Replace).

Your Own Function: mergeWith()

For anything else, mergeWith() takes your own function that combines the later value with the earlier one. Let's say logging should stay on once any layer turns it on:

Expect::bool()->mergeWith(fn($value, $base) => $value || $base); // once enabled, stays enabled

The function only combines layers; to adjust the shape of a single layer, use before().

version: 2.x 1.x