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']);