PHP 8.x and Later Development

PHP 7.x is at its end of life, and so development is moving to PHP 8.x on future releases of pfSense software, such as pfSense Plus software version 23.01 and pfSense CE software version 2.7.x.

Migrating to PHP 8.1 introduces some backward incompatible changes from 7.x in how some expressions are evaluated.

Configuration Access

One of the more pervasive idioms in the pfSense code base and its packages that is now problematic is the method by which code historically traverses the configuration tree. The problematic method indexes the configuration data into multiple levels of nested associative arrays, e.g.:

$foo = $config['section']['subsection']['item'];

The configuration array tree is not fully populated with all possible keys, therefore any of the keys in the expression above may not exist in the array being indexed. The evaluation of an expression indexing an associative array with a key that it does not contain results in an empty string. Expressions indexing a string are allowed in PHP 7.4, and in the case that the key is a string the expression evaluates to an empty string again. However, in PHP 8.1 these expressions now raise errors and terminate the evaluation of PHP code. To correct these errors and to progress toward decoupling the implementation of the runtime configuration store from the rest of the code base, the development tree now includes utility functions to access the $config variable instead of accessing it directly. These functions are defined in /etc/inc/config.lib.inc.

Reading Configuration Values

The following function should be used to read values from the configuration:

config_get_path(string $path, $default = null)

Given a path with the separator /, look for an item by indexing into the configuration tree by the identifiers in the path.

Returns $default if the item or any intermediary in the path cannot be found.

Examples

Basic example accessing one value nested inside a section:

PHP 7.x Style:

$foo = $config['section']['item'];

PHP 8.x Style:

$foo = config_get_path('section/item');

More complicated example accessing a value inside a subsection multiple levels deep:

PHP 7.x Style:

init_config_arr('section', 'subsection');
if ($config['section']['subsection']['item']) {
    $foo = $config['section']['subsection']['item'];
} else {
    $foo = "Not Found";
}

PHP 8.x Style:

$foo = config_get_path('section/subsection/item', "Not Found");

Testing if Settings are Enabled

A common way to mark options and features as enabled or disabled in the configuration is via “presence” style variables. Where if the value is present in the configuration, the option is enabled. If the value is missing, it’s disabled.

There is a new shortcut for testing these options:

config_path_enabled($path, $enable_key = "enable")

This function determines if $enable_key is a key into the array at path $path, and the value is non-null. This is used to obtain the same semantics as checking if the value is present in the configuration, but in a safe manner.

Note

Some sections of the configuration use different semantics for indicating if a feature is enabled which config_path_enabled() does not interpret at this time. Only use config_path_enabled() to replace isset() style expressions.

Examples

This first example is for an option which is enabled or disabled based on the presence of the a config element named default “enable”:

PHP 7.x Style:

$bar_enabled = isset($config['foo']['bar']['enable']);

PHP 8.x Style:

$bar_enabled = config_path_enabled('foo/bar');

This next example tests whether or not the element “baz” is present in the configuration at the same level of the previous example.

PHP 7.x Style:

$baz_enabled = isset($config['foo']['bar']['baz']);

PHP 8.x Style:

$baz_enabled = config_path_enabled('foo/bar', 'baz');

Writing Configuration Values

This function sets an item at a given the configuration path:

config_set_path(string $path, $value, $default = null)

If any intermediate section in the path cannot be found, or is an empty value, the function creates an array for it. If any intermediary in the path is unexpectedly a scalar value, the function returns the $default value to indicate an error.

Examples

This basic example sets a value in the configuration and then writes the changes:

PHP 7.x Style:

$config['foo']['bar']['item'] = 'newvalue';
write_config('Update settings');

PHP 8.x Style:

config_set_path('foo/bar/item', "newvalue");
write_config('Update settings');

This slightly more complex example reads a value from the configuration, updates the option with a new value, then stores the results.

PHP 7.x Style:

$bar = &$config['foo']['bar'];
/* ... */
$bar['item'] = 'newvalue';
/* ... */
write_config('Update settings');

PHP 8.x Style:

$bar = config_get_path('foo/bar');
/* ... */
$bar['item'] = 'value';
/* ... */
config_set_path('foo/bar', $bar);
write_config('Update settings');

Deleting Configuration Values

To unset (delete) a value from the configuration, use this function:

config_del_path(string $path)

This function completely removes a path from the configuration tree by running unset() on the array element, if it exists.

This is obtains the same semantic as unset() in a safe manner.

Examples

PHP 7.x Style:

unset($config['foo']['bar']['item']);

PHP 8.x Style:

config_del_path('foo/bar/item');

Array Access Functions

The configuration-based functions utilize more general array functions which can be used directly for similar tasks on any array.

array_get_path(array &$arr, string $path, $default = null)
array_set_path(array &$arr, string $path, $value, $default = null)
array_path_enabled(array &$arr, string $path, $enable_key = “enable”)
array_del_path(array &$arr, string $path)

String to Number Comparison

In the past, PHP would return true when comparing 0 to an empty string ('') but now it returns false. If the code in question must test against an empty string to know if a value is usable, do not cast the value to int or take other equivalent actions. Along the same lines, if the value must be compared numerically, cast it to int after first checking if it’s an empty or otherwise usable value.

Note

While the behavior of comparing 0 to an empty string has changed, the result of empty(0) and empty('0') are still true on PHP 8.x.

Before:

$varusersamountoftime = ($users['varusersamountoftime'] ?: '');
$varusersamountoftime = (int) $varusersamountoftime * 60;
/* ... */
if ($varusersamountoftime != '') {
    /* ... */
}

In this example the value of the variable is always cast to int which means that an empty value is changed to 0. On PHP 7.x, the later if would evaluate to false if the value was 0 but on PHP 8.x, the test will evaluate to true.

After:

if ($users['varusersamountoftime']) {
    $varusersamountoftime = (int) $users['varusersamountoftime'] * 60;
} else {
    $varusersamountoftime = '';
}
/* ... */
if ($varusersamountoftime != '') {
    /* ... */
}

This code will evaluate the same on both PHP 7.x and 8.x because it only changes the value to int when the variable is set and has a non-zero value, and it is an empty string otherwise.

Practical Examples

Iterating over Arrays of Items in the Configuration

Previously, one or more checks would have to be made to see if the array in the configuration is present, checking the value with isset() or is_array(). With the config interface, a suitable default value of an empty array can be specified in a call to config_get_path(), and the result can be directly iterated. The resulting code is more concise.

This example iterates over aliases in the configuration:

PHP 7.x Style:

if (isset($config['aliases']) &&
    is_array($config['aliases']) &&
    isset($config['aliases']['alias']) &&
    is_array($config['aliases']['alias'])) {
    foreach ($config['aliases']['alias'] as $aliased) {
        if ($aliased['name'] == $alias_name) {
            return filter_generate_nested_alias($aliased['name']);
        }
    }
}

The above example needs multiple safety checks to ensure that each level exists and is an array before it attempts to iterate over the array. Some of this can be bypassed by running a init_config_arr() or similar but it still requires wrapping it in a test before iterating.

Contrast that with this much simpler example using the new utility function:

PHP 8.x Style:

foreach (config_get_path('aliases/alias', []) as $aliased) {
    if ($aliased['name'] == $alias_name) {
        return filter_generate_nested_alias($aliased['name']);
    }
}

This next example demonstrates a common method used to check all OpenVPN server and client instances:

PHP 7.x Style:

init_config_arr(array('openvpn'));
foreach (array('server', 'client') as $mode) {
    if (is_array($config['openvpn']["openvpn-{$mode}"])) {
        foreach ($config['openvpn']["openvpn-{$mode}"] as $id => $setting) {
            /* ... */
        }
    }
}

This becomes much simpler and does not require the extra initialization or tests:

PHP 8.x Style:

foreach (array('server', 'client') as $mode) {
    foreach (config_get_path("openvpn/openvpn-{$mode}", []) as $id => $setting) {
        /* ... */
    }
}

This example iterates over all installed packages. This requires extra checks because the user may not have any packages installed, or may even not have a section in the configuration with package settings.

PHP 7.x Style:

if (is_array($config['installedpackages']) &&
    is_array($config['installedpackages']['package'])) {
    foreach ($config['installedpackages']['package'] as $pkg) {
        if ($pkg['name'] == $package_name) {
            return $pkg['descr'];
        }
    }
}

PHP 8.x Style:

foreach (config_get_path('installedpackages/package', []) as $pkg) {
   if ($pkg['name'] == $package_name) {
      return $pkg['descr'];
   }
}

Replacing isset() to Determine if an Item is Enabled

As with other configuration accesses, using config_path_enabled() is safe if any part of the path does not exist, and will return false if that is the case. If the array key for the element that indicates something is enabled is not the default enable, the name can be passed as another parameter.

PHP 7.x Style:

$assignedif = convert_real_interface_to_friendly_interface_name($vlanif);
if ($assignedif) {
    if (isset($config['interfaces'][$assignedif]['enable'])) {
        interface_configure($assignedif, true);
    }
}

PHP 8.x Style:

$assignedif = convert_real_interface_to_friendly_interface_name($vlanif);
if ($assignedif) {
    if (config_path_enabled("interfaces/{$assignedif}")) {
        interface_configure($assignedif, true);
    }
}

This example checks if an option is enabled the same way, but using a specific key name:

PHP 7.x Style:

if (!isset($config['system']['ipv6allow'])) {
    /* ... */
}

PHP 8.x Style:

if (!config_path_enabled('system','ipv6allow')) {
    /* ... */
}

Accessing Items from Variable Paths

Before, extra care needed to be taken to initialize multi-level arrays and to check before accessing certain areas, for example:

init_config_arr(array('captiveportal'));
$a_cp = &$config['captiveportal'];
/* ... */
if ($a_cp[$cpzone]) {
    /* ... */
    $pconfig['certref'] = $a_cp[$cpzone]['certref'];
    /* ... */
}

Now this can be done safely without the extra steps:

$pconfig['certref'] = config_get_path("captiveportal/{$cpzone}/certref");

Note

Note the use of double quotes in the path to allow variable substitution.

Default Values

One of the primary benefits of this new style is the ability to easily accommodate default values without a lot of extra logic. There are examples of this in previous sections above where a default array is returned ([]) to ensure that a returned value is always an array.

In this example, a value is populated either from the configuration or using a globally defined default:

PHP 7.x Style:

init_config_arr('syslog');
$syslogcfg = $config['syslog'];
$log_size = isset($syslogcfg['logfilesize']) ? $syslogcfg['logfilesize'] : $g['default_log_size'];

PHP 8.x Style:

$log_size = config_get_path('syslog/logfilesize', $g['default_log_size']);