Configuration Closures

February 6th, 2021 Code , PHP
Photo by Luca Bravo on Unsplash
Photo by Luca Bravo on Unsplash

Nearly every application in existence requires some form of configuration. After all, no two instances of the same app are exactly the same. The "tried and true" (read "quick and dirty") way of doing this has conventionally been with arrays. This works well for most basic configuration values of scalar types, however, sometimes it may be necessary to configure complex objects.

An Example

A common example of complex object configuration is when configuring application caching or queuing. Enabling these features will often require a Redis or Memcached server for storing your cached items or queued jobs. In PHP this is accomplished via a Redis or Memcached object.

Let's take a look at a typical Redis server configuration. First, the configuration file.

'redis_config' => [
    'host' => env('REDIS_HOST', '127.0.0.1'),
    'port' => env('REDIS_PORT', 6379),
    'password' => env('REDIS_PASSWORD'),
    // Many more possible options...
]

Then in the application code...

// Instantiate a new Redis instance
$redis = new Redis;

// Retrieve the array from the config
$redisConfig = $config['redis_config'];

// Configure the Redis object
$redis->pconnect($redisConfig['host'], $redisConfig['port']);
$redis->auth($redisConfig['password']);
// And so on for every defined option...

// Use the configured object
$redis->set($key, $value, $expiration);
$redis->get($key);

This is fine if users only ever needs to configure a few options. However, when the specific options a user will require is unknown ahead of time the only way to guarantee compatibility is to pre-define every possible option. This would be excessively verbose, especially when most users will only configure a few options. Additionally, if the available Redis configuration options were to ever change the configuration options would need to be updated to reflect those changes.

In this context "user" refers to the person configuring the application.

A Better Approach

When configuration of a complex object (e.g. Memcached, Redis, etc.) is required use a "configuration closure" instead of defining a list of individual configuration options. Let's see how this works in practice.

First, in the configuration file add an entry with a closure as its value. This closure should accept an instance of our complex object and may contain some sane defaults in the body of the closure.

'redis_config' => function (Redis $redis): void {
    // User configures the Redis object here
    $redis->pconnect('localhost', 6379);
    $redis->auth(env('REDIS_PASSWORD'));
    // Any additional configuration...
}

This gives the user direct access to the actual Redis object we'll be using in the application. They can then configure that object however they require without us needing to clutter the configuration with several unused options.

Next, in the application code we resolve the closure from the config, execute it and use the configured object normally.

// Retrieve the closure from the config
$redisConfig = $config['redis_config'];

// Execute the closure to configure the object
$redisConfig($redis = new Redis);

// Use the configured object
$redis->set($key, $value, $expiration);
$redis->get($key);

In summary, configuration closures give the end-user full control of the configuration of complex objects, eliminates the need to pre-define options, future proofs our configuration, requires less code and (bonus) is type safe!


For more information about closures check out the official docs or PHP The Right Way.