Chapter 6: Understanding PHP 8 Functional Differences

In this chapter, you will learn about potential backward-compatible breaks at the PHP 8 command, or functional, level. This chapter presents important information that highlights potential pitfalls when migrating existing code to PHP 8. The information presented in this chapter is critical to know so that you can produce reliable PHP code. After working through the concepts in this chapter, you'll be in a better position to write code that produces precise results and avoids inconsistencies.

Topics covered in this chapter include the following:

  • Learning key advanced string handling differences
  • Understanding PHP 8 string-to-numeric comparison improvements
  • Handling differences in arithmetic, bitwise, and concatenation operations
  • Taking advantage of locale independence
  • Handling arrays in PHP 8
  • Mastering changes in security functions and settings

Technical requirements

To examine and run the code examples provided in this chapter, the minimum recommended hardware is the following:

  • An x86_64-based desktop PC or laptop
  • 1 gigabyte (GB) of free disk space
  • 4 GB of RAM
  • A 500 kilobits per second (Kbps) or faster internet connection

In addition, you will need to install the following software:

  • Docker
  • Docker Compose

Please refer to the Technical requirements section in Chapter 1, Introducing New PHP 8 OOP Features, for more information on Docker and Docker Compose installation, as well as how to build the Docker container used to demonstrate the code explained in this book. In this book, we refer to the directory in which you restored the sample code for this book as /repo.

The source code for this chapter is located here:

https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices.

We can now begin our discussion by examining the differences in string handling introduced in PHP 8.

Learning key advanced string handling differences

String functions in general have been tightened and normalized in PHP 8. You will find that usage is more heavily restricted in PHP 8, which ultimately forces you to produce better code. We can say that the nature and order of string function arguments is much more uniform in PHP 8, which is why we say that the PHP core team has normalized usage.

These improvements are especially evident when dealing with numeric strings. Other changes in PHP 8 string handling involve minor changes to arguments. In this section, we introduce you to the key changes in how PHP 8 handles strings.

It's important to understand not only the handling improvements introduced in PHP 8 but also to understand the deficiencies in string handling prior to PHP 8.

Let's first have a look at an aspect of PHP 8 string handling in functions that search for embedded strings.

Handling changes to the needle argument

A number of PHP string functions search for the presence of a substring within a larger string. These functions include strpos(), strrpos(), stripos(), strripos(), strstr(), strchr(), strrchr(), and stristr(). All of these functions have these two parameters in common: the needle and the haystack.

Differentiating between the needle and the haystack

To illustrate the difference between the needle and the haystack, have a look at the function signature for strpos():

strpos(string $haystack,string $needle,int $pos=0): int|false

$haystack is the target of the search. $needle is the substring to be sought. The strpos() function returns the position of the substring within the search target. If the substring is not found, the Boolean FALSE is returned. The other str*() functions produce different types of output that we will not detail here.

Two key changes in how PHP 8 handles the needle argument have the potential to break an application migrated to PHP 8. These changes apply to situations where the needle argument is not a string or where the needle argument is empty. Let's have a look at non-string needle argument handling first.

Dealing with non-string needle arguments

Your PHP application might not be taking the proper precautions to ensure that the needle argument to the str*() functions mentioned here is always a string. If that is the case, in PHP 8, the needle argument will now always be interpreted as a string rather than an ASCII code point.

If you need to supply an ASCII value, you must use the chr() function to convert it to a string. In the following example, the ASCII value for LF (" ") is used instead of a string. In PHP 7 or below, strpos() performs an internal conversion before running the search. In PHP 8, the number is simply typecast into a string, yielding unexpected results.

Here is a code example that searches for the presence of LF within a string. However, note that instead of providing a string as an argument, an integer with a value of 10 is provided:

// /repo/ch06/php8_num_str_needle.php

function search($needle, $haystack) {

    $found = (strpos($haystack, $needle))

           ? 'contains' : 'DOES NOT contain';

    return "This string $found LF characters ";

}

$haystack = "We're looking For linefeeds In this

             string ";

$needle = 10;         // ASCII code for LF

echo search($needle, $haystack);

Here are the results of the code sample running in PHP 7:

root@php8_tips_php7 [ /repo/ch06 ]#

php php8_num_str_needle.php

This string contains LF characters

And here are the results of the same code block running in PHP 8:

root@php8_tips_php8 [ /repo/ch06 ]#

php php8_num_str_needle.php

This string DOES NOT contain LF characters

As you can see, comparing the output in PHP 7 with the output in PHP 8, the same code block yields radically different results. This is an extremely difficult potential code break to spot as no Warnings or Errors are generated.

The best practice is to apply a string type hint to the needle argument of any function or method that incorporates one of the PHP str*() functions. If we rewrite the previous example, the output is consistent in both PHP 7 and PHP 8. Here is the same example rewritten using a type hint:

// /repo/ch06/php8_num_str_needle_type_hint.php

declare(strict_types=1);

function search(string $needle, string $haystack) {

    $found = (strpos($haystack, $needle))

           ? 'contains' : 'DOES NOT contain';

    return "This string $found LF characters ";

}

$haystack = "We're looking For linefeeds In this

             string ";

$needle   = 10;         // ASCII code for LF

echo search($needle, $haystack);

Now, in either version of PHP, this is the output:

PHP Fatal error:  Uncaught TypeError: search(): Argument #1 ($needle) must be of type string, int given, called in /repo/ch06/php8_num_str_needle_type_hint.php on line 14 and defined in /repo/ch06/php8_num_str_needle_type_hint.php:4

By declaring strict_types=1, and by adding a type hint of string before the $needle argument, any developer who misuses your code receives a clear indication that this practice is not acceptable.

Let's now have a look at what happens in PHP 8 when the needle argument is missing.

Handling empty needle arguments

Another major change in the str*() function is that the needle argument can now be empty (for example, anything that would make the empty() function return TRUE). This presents significant potential for backward compatibility breaks. In PHP 7, if the needle argument is empty, the return value from strpos() would be the Boolean FALSE, whereas, in PHP 8, the empty value is first converted to a string, thereby producing entirely different results.

It's extremely important to be aware of this potential code break if you plan to update your PHP version to 8. An empty needle argument is difficult to spot when reviewing code manually. This is a situation where a solid set of unit tests is needed to ensure a smooth PHP migration.

To illustrate the potential problem, consider the following example. Assume that the needle argument is empty. In this situation, a traditional if() check to see whether the strpos() result is not identical to FALSE produces different results between PHP 7 and 8. Here is the code example:

  1. First, we define a function that reports whether or not the needle value is found in the haystack using strpos(). Note the strict type check against the Boolean FALSE:

    // php7_num_str_empty_needle.php

    function test($haystack, $search) {

        $pattern = '%15s | %15s | %10s' . " ";

        $result  = (strpos($haystack, $search) !== FALSE)

                 ? 'FOUND' :  'NOT FOUND';

        return sprintf($pattern,

               var_export($search, TRUE),

               var_export(strpos($haystack, $search),

                 TRUE),

               $result);

    };

  2. We then define the haystack as a string with letters and numbers. The needle argument is provided in the form of an array of values that are all considered empty:

    $haystack = 'Something Anything 0123456789';

    $needles = ['', NULL, FALSE, 0];

    foreach ($needles as $search)

        echo test($haystack, $search);

    The output in PHP 7 appears as follows:

root@php8_tips_php7 [ /repo/ch06 ]#

php php7_num_str_empty_needle.php

PHP Warning:  strpos(): Empty needle in /repo/ch06/php7_num_str_empty_needle.php on line 5

// not all Warnings are shown ...

             '' |           false |  NOT FOUND

           NULL |           false |  NOT FOUND

          false |           false |  NOT FOUND

              0 |           false |  NOT FOUND

After a set of Warnings, the final output appears. As you can see from the output, the return value from strpos($haystack, $search) is consistently the Boolean FALSE in PHP 7.

The output running the same code in PHP 8, however, is radically different. Here is the output from PHP 8:

root@php8_tips_php8 [ /repo/ch06 ]#

php php7_num_str_empty_needle.php

             '' |               0 |      FOUND

           NULL |               0 |      FOUND

          false |               0 |      FOUND

              0 |              19 |      FOUND

In PHP 8, the empty needle argument is first silently converted to a string. None of the needle values return the Boolean FALSE. This causes the function to report that the needle has been found. This is certainly not the desired result. In the case of the number 0, however, it is contained in the haystack, resulting in a value of 19 being returned.

Let's have a look at how this problem might be addressed.

Solving the problem using str_contains()

The intent of the code block shown in the previous section is to determine whether or not the haystack contains the needle. strpos() is not the right tool to accomplish this task! Have a look at the same function using str_contains() instead:

// /repo/ch06/php8_num_str_empty_needle.php

function test($haystack, $search) {

    $pattern = '%15s | %15s | %10s' . " ";

    $result  = (str_contains($search, $haystack) !==  

                FALSE)  

                 ? 'FOUND'  : 'NOT FOUND';

    return sprintf($pattern,

           var_export($search, TRUE),

           var_export(str_contains($search, $haystack),

             TRUE),

           $result);

};

If we then run the modified code in PHP 8, we get results similar to those received from PHP 7:

root@php8_tips_php8 [ /repo/ch06 ]#

php php8_num_str_empty_needle.php

             '' |           false |  NOT FOUND

           NULL |           false |  NOT FOUND

          false |           false |  NOT FOUND

              0 |           false |  NOT FOUND

You might ask why is it that the number 0 is not found in the string? The answer is that str_contains() does a stricter search. Integer 0 is not the same as the string "0"! Let's now have a look at the v*printf() family; another family of string functions that exerts stricter control over its arguments in PHP 8.

Dealing with v*printf() changes

The v*printf() family of functions is a subset of the printf() family of functions that include vprintf(), vfprintf(), and vsprintf(). The difference between this subset and the main family is that the v*printf() functions are designed to accept an array as an argument rather than an unlimited series of arguments. Here is a simple example that illustrates the difference:

  1. First, we define a set of arguments that will be inserted into a pattern, $patt:

    // /repo/ch06/php8_printf_vs_vprintf.php

    $ord  = 'third';

    $day  = 'Thursday';

    $pos  = 'next';

    $date = new DateTime("$ord $day of $pos month");

    $patt = "The %s %s of %s month is: %s ";

  2. We then execute a printf() statement using a series of arguments:

    printf($patt, $ord, $day, $pos,

           $date->format('l, d M Y'));

  3. We then define the arguments as an array, $arr, and use vprintf() to produce the same result:

    $arr  = [$ord, $day, $pos, $date->format('l, d M

               Y')];vprintf($patt, $arr);

    Here is the output of the program running in PHP 8. The output is the same running in PHP 7 (not shown):

    root@php8_tips_php8 [ /repo/ch06 ]#

    php php8_printf_vs_vprintf.php

    The third Thursday of next month is: Thursday, 15 Apr 2021

    The third Thursday of next month is: Thursday, 15 Apr 2021

    As you can see, the output of both functions is identical. The only usage difference is that vprintf() accepts the parameters in the form of an array.

Prior versions of PHP allowed a developer to play fast and loose with arguments presented to the v*printf() family of functions. In PHP 8, the data type of the arguments is now strictly enforced. This only presents a problem where code controls do not exist to ensure that an array is presented. Another even more important difference is that PHP 7 will allow ArrayObject with v*printf(), whereas PHP 8 will not.

In the example shown here, PHP 7 issues a Warning, whereas PHP 8 throws an Error:

  1. First, we define the pattern and the source array:

    // /repo/ch06/php7_vprintf_bc_break.php

    $patt = " %s. %s. %s. %s. %s.";

    $arr  = ['Person', 'Woman', 'Man', 'Camera', 'TV'];

  2. We then define a test data array in order to test which arguments are accepted by vsprintf():

    $args = [

        'Array' => $arr,

        'Int'   => 999,

        'Bool'  => TRUE,

        'Obj'   => new ArrayObject($arr)

    ];

  3. We then define a foreach() loop that goes through the test data and exercises vsprintf():

    foreach ($args as $key => $value) {

        try {

            echo $key . ': ' . vsprintf($patt, $value);

        } catch (Throwable $t) {

            echo $key . ': ' . get_class($t)

                 . ':' . $t->getMessage();

        }

    }

Here is the output running in PHP 7:

root@php8_tips_php7 [ /repo/ch06 ]#

php php7_vprintf_bc_break.php

Array:     Person. Woman. Man. Camera. TV.

PHP Warning:  vsprintf(): Too few arguments in /repo/ch06/php8_vprintf_bc_break.php on line 14

Int:

PHP Warning:  vsprintf(): Too few arguments in /repo/ch06/php8_vprintf_bc_break.php on line 14

Bool:

Obj:     Person. Woman. Man. Camera. TV.

As you can see from the output, both the array and ArrayObject arguments are accepted in PHP 7. Here is the same code example running in PHP 8:

root@php8_tips_php8 [ /repo/ch06 ]#

php php7_vprintf_bc_break.php

Array:     Person. Woman. Man. Camera. TV.

Int: TypeError:vsprintf(): Argument #2 ($values) must be of type array, int given

Bool: TypeError:vsprintf(): Argument #2 ($values) must be of type array, bool given

Obj: TypeError:vsprintf(): Argument #2 ($values) must be of type array, ArrayObject given

As expected, the PHP 8 output is much more consistent. In PHP 8, the v*printf() functions are strictly typed to accept only an array as an argument. Unfortunately, there's a real possibility you may have been using ArrayObject. This is easily addressed by simply using the getArrayCopy() method on the ArrayObject instance, which returns an array.

Here is the rewritten code that works in both PHP 7 and PHP 8:

    if ($value instanceof ArrayObject)

        $value = $value->getArrayCopy();

    echo $key . ': ' . vsprintf($patt, $value);

Now that you have an idea where to look for a potential code break when using the v*printf() functions, let's turn our attention to differences in how string functions with a null length argument work in PHP 8.

Working with null length arguments in PHP 8

In PHP 7 and earlier, a NULL length argument resulted in an empty string. In PHP 8, a NULL length argument is now treated the same as if the length argument is omitted. Functions affected include the following:

  • substr()
  • substr_count()
  • substr_compare()
  • iconv_substr()

In the example shown next, PHP 7 returns an empty string whereas PHP 8 returns the remainder of the string. This has a high potential for a code break if the result of the operation is used to confirm or deny the existence of the substring:

  1. First, we define a haystack and needle. We then run strpos() to get the position of the needle in the haystack:

    // /repo/ch06/php8_null_length_arg.php

    $str = 'The quick brown fox jumped over the fence';

    $var = 'fox';

    $pos = strpos($str, $var);

  2. Next, we pull out the substring, deliberately leaving the length argument undefined:

    $res = substr($str, $pos, $len);

    $fnd = ($res) ? '' : ' NOT';

    echo "$var is$fnd found in the string ";

Here is the output running in PHP 7:

root@php8_tips_php7 [ /repo/ch06 ]#

php php8_null_length_arg.php

PHP Notice:  Undefined variable: len in /repo/ch06/php8_null_length_arg.php on line 8

Result   : fox is NOT found in the string

Remainder:

As expected, PHP 7 issues a Notice. However, as an empty string is returned due to the NULL length argument, the search result is incorrect. Here is the same code running in PHP 8:

root@php8_tips_php8 [ /repo/ch06 ]#

php php8_null_length_arg.php

PHP Warning:  Undefined variable $len in /repo/ch06/php8_null_length_arg.php on line 8

Result   : fox is found in the string

Remainder: fox jumped over the fence

PHP 8 issues a Warning and returns the remainder of the string. This is consistent with the behavior where the length argument is entirely omitted. If your code relies upon an empty string being returned, a potential code break exists after a PHP 8 update.

Let's now have a look at another situation where PHP 8 has made string handling more uniform in the implode() function.

Examining changes to implode()

Two widely used PHP functions perform array to string conversion and the reverse: explode() converts a string to an array, and implode() converts an array to a string. However, there lurks a deep dark secret with the implode() function: its two parameters can be expressed in any order!

Please bear in mind that when PHP was first introduced in 1994, the initial goal was to make it as easy to use as possible. This approach succeeded, to the point where PHP is the language of choice on over 78% of all web servers today according to a recent survey of server-side programming languages conducted by w3techs. (https://w3techs.com/technologies/overview/programming_language)

However, in the interests of consistency, it makes sense to align the parameters of the implode() function with its mirror twin, explode(). Accordingly, arguments supplied to implode() must now be in this order:

implode(<GLUE STRING>, <ARRAY>);

Here is the code example that calls the implode() function with arguments in either order:

// /repo/ch06/php7_implode_args.php

$arr  = ['Person', 'Woman', 'Man', 'Camera', 'TV'];

echo __LINE__ . ':' . implode(' ', $arr) . " ";

echo __LINE__ . ':' . implode($arr, ' ') . " ";

As you can see from the PHP 7 output below, both echo statements produce results:

root@php8_tips_php7 [ /repo/ch06 ]# php php7_implode_args.php

5:Person Woman Man Camera TV

6:Person Woman Man Camera TV

In PHP 8, only the first statement succeeds, as shown here:

root@php8_tips_php8 [ /repo/ch06 ]#

php php7_implode_args.php

5:Person Woman Man Camera TV

PHP Fatal error:  Uncaught TypeError: implode(): Argument #2 ($array) must be of type ?array, string given in /repo/ch06/php7_implode_args.php:6

It will be extremely difficult to spot where implode() is receiving parameters in the wrong order. The best way to be forewarned prior to a PHP 8 migration would be to make a note of all classes of PHP files that use implode(). Another suggestion would be to take advantage of the PHP 8 named arguments feature (covered in Chapter 1, Introducing New PHP 8 OOP Features).

Learning about constants usage in PHP 8

One of the truly outrageous capabilities of PHP prior to version 8 was the ability to define case-insensitive constants. In the beginning, when PHP was first introduced, many developers were writing lots of PHP code with a notable absence of any sort of coding standard. The objective at the time was just to make it work.

In line with the general trend toward enforcing good coding standards, this ability was deprecated in PHP 7.3 and removed in PHP 8. A backward-compatible break might appear if you are using define() with the third parameter set to TRUE.

The example shown here works in PHP 7, but not entirely in PHP 8:

// /repo/ch06/php7_constants.php

define('THIS_WORKS', 'This works');

define('Mixed_Case', 'Mixed Case Works');

define('DOES_THIS_WORK', 'Does this work?', TRUE);

echo __LINE__ . ':' . THIS_WORKS . " ";

echo __LINE__ . ':' . Mixed_Case . " ";

echo __LINE__ . ':' . DOES_THIS_WORK . " ";

echo __LINE__ . ':' . Does_This_Work . " ";

In PHP 7, all lines of code work as written. Here is the output:

root@php8_tips_php7 [ /repo/ch06 ]# php php7_constants.php

7:This works

8:Mixed Case Works

9:Does this work?

10:Does this work?

Please note that the third argument of define() was deprecated in PHP 7.3. Accordingly, if you run this code example in PHP 7.3 or 7.4, the output is identical with the addition of a Deprecation notice.

In PHP 8, however, quite a different result is produced, as shown here:

root@php8_tips_php8 [ /repo/ch06 ]# php php7_constants.php

PHP Warning:  define(): Argument #3 ($case_insensitive) is ignored since declaration of case-insensitive constants is no longer supported in /repo/ch06/php7_constants.php on line 6

7:This works

8:Mixed Case Works

9:Does this work?

PHP Fatal error:  Uncaught Error: Undefined constant "Does_This_Work" in /repo/ch06/php7_constants.php:10

As you might expect, lines 7, 8, and 9 produce the expected result. The last line, however, throws a fatal Error, because constants in PHP 8 are now case-sensitive. Also, a Warning is issued for the third define() statement as the third parameter is ignored in PHP 8.

You now have an idea about key string handling differences introduced in PHP 8. We next turn our attention to changes in how numeric strings are compared with numbers.

Understanding PHP 8 string-to-numeric comparison improvements

Comparing two numeric values has never been an issue in PHP. A comparison between two strings is also not an issue. A problem arises in non-strict comparisons between strings and numeric data (hardcoded numbers, or variables containing data of the float or int type). In such cases, PHP will always convert the string to a numeric value if a non-strict comparison is executed.

The only time a string-to-numeric conversion is 100% successful is when the string only contains numbers (or numeric values such as plus, minus, or the decimal separator). In this section, you learn how to protect against inaccurate non-strict comparisons involving strings and numeric data. Mastering the concepts presented in this chapter is critical if you wish to produce code with consistent and predictable behavior.

Before we get into the details of string-to-numeric comparisons, we need to first gain an understanding of what is meant by a non-strict comparison.

Learning about strict and non-strict comparisons

The concept of type juggling is an essential part of the PHP language. This capability was built into the language literally from its first day. Type juggling involves performing an internal data type conversion before performing an operation. This ability is critical to the success of the language.

PHP was originally devised to perform in a web environment and needed a way to handle data transmitted as part of an HTTP packet. HTTP headers and bodies are transmitted as text and are received by PHP as strings stored in a set of super-globals, including $_SERVER, $_GET, $_POST, and so forth. Accordingly, the PHP language needs a quick way to deal with string values when performing operations that involve numbers. This is the job of the type-juggling process.

A strict comparison is one that first checks the data type. If the data types match, the comparison proceeds. Operators that invoke a strict comparison include === and !==, among others. Certain functions have an option to enforce a strict data type. One example is in_array(). If the third argument is set to TRUE, a strict-type search ensues. Here is the method signature for in_array():

in_array(mixed $needle, array $haystack, bool $strict = false)

A non-strict comparison is where no data type check is made prior to comparison. Operators that perform non-strict comparisons include ==, !=, <, and >, among others. It's worth noting that the switch {} language construct performs non-strict comparisons in its case statements. Type juggling is performed if a non-strict comparison is made that involves operands of different data types.

Let's now have a detailed look at numeric strings.

Examining numeric strings

A numeric string is a string that contains only numbers or numeric characters, such as the plus sign (+), minus sign (-), and decimal separator.

Important note

It should be noted that PHP 8 internally uses the period character (.) as the decimal separator. If you need to render numbers in locales that do not use the period as a decimal separator (for example, in France, the comma (,) is used as the decimal separator), use the number_format() function (see https://www.php.net/number_format). Please have a look at the Taking advantage of locale independence section in this chapter for more information.

Numeric strings can also be composed using engineering notation (also called scientific notation). A non-well-formed numeric string is a numeric string containing values other than digits, the plus sign, minus sign, or decimal separator. A leading-numeric string starts with a numeric string but is followed by non-numeric characters. Any string that is neither numeric nor leading-numeric is considered non-numeric by the PHP engine.

In previous versions of PHP, type juggling inconsistently parsed strings containing numbers. In PHP 8, only numeric strings can be cleanly converted to a number: no leading or trailing whitespace or other non-numeric characters can be present.

As an example, have a look at the difference in how PHP 7 and 8 handle numeric strings in this code sample:

// /repo/ch06/php8_num_str_handling.php

$test = [

    0 => '111',

    1 => '   111',

    2 => '111   ',

    3 => '111xyz'

];

$patt = "%d : %3d : '%-s' ";

foreach ($test as $key => $val) {

    $num = 111 + $val;

    printf($patt, $key, $num, $val);

}

Here is the output running in PHP 7:

root@php8_tips_php7 [ /repo/ch06 ]#

php php8_num_str_handling.php

0 : 222 : '111'

1 : 222 : '   111'

PHP Notice:  A non well formed numeric value encountered in /repo/ch06/php8_num_str_handling.php on line 11

2 : 222 : '111   '

PHP Notice:  A non well formed numeric value encountered in /repo/ch06/php8_num_str_handling.php on line 11

3 : 222 : '111xyz'

As you can see from the output, PHP 7 considers a string with a trailing space to be non-well-formed. However, a string with a leading space is considered well-formed and passes through without generating a Notice. A string with non-whitespace characters is still processed but merits a Notice.

Here is the same code example running in PHP 8:

root@php8_tips_php8 [ /repo/ch06 ]#

php php8_num_str_handling.php

0 : 222 : '111'

1 : 222 : '   111'

2 : 222 : '111   '

PHP Warning:  A non-numeric value encountered in /repo/ch06/php8_num_str_handling.php on line 11

3 : 222 : '111xyz'

PHP 8 is much more consistent in that numeric strings that contain either leading or trailing spaces are treated equally, and no Notices or Warnings are generated. However, the last string, formerly a Notice in PHP 7, now generates a Warning.

Tip

You can read about numeric strings in the PHP documentation here:

https://www.php.net/manual/en/language.types.numeric-strings.php

For more information on type juggling, have a look at the following URL:

https://www.php.net/manual/en/language.types.type-juggling.php

Now that you have an idea of what is considered a well-formed and non-well-formed numeric string, let's turn our attention to the more serious issue of potential backward-compatible breaks when dealing with numeric strings in PHP 8.

Detecting backward-compatible breaks involving numeric strings

You must understand where there is potential for your code to break following a PHP 8 upgrade. In this subsection, we show you a number of extremely subtle differences that can have large consequences.

Potential code breaks could surface any time a non-well-formed numeric string is used:

  • With is_numeric()
  • In a string offset (for example, $str['4x'])
  • With bitwise operators
  • When incrementing or decrementing a variable whose value is a non-well-formed numeric string

Here are some suggestions to fix your code:

  • Consider using trim() on numeric strings that might include leading or trailing white space (for example, numeric strings embedded within posted form data).
  • If your code relies upon strings that start with a number, use an explicit typecast to ensure that the number is correctly interpolated.
  • Do not rely upon an empty string (for example, $str = '') to cleanly convert to 0.

In this following code example, a non-well-formed string with a trailing space is assigned to $age:

// /repo/ch06/php8_num_str_is_numeric.php

$age = '77  ';

echo (is_numeric($age))

     ? "Age must be a number "

     : "Age is $age ";

When we run this code in PHP 7, is_numeric() returns TRUE. Here is the PHP 7 output:

root@php8_tips_php7 [ /repo/ch06 ]#

php php8_num_str_is_numeric.php

Age is 77  

On the other hand, when we run this code in PHP 8, is_numeric() returns FALSE as the string is not considered numeric. Here is the PHP 8 output:

root@php8_tips_php8 [ /repo/ch06 ]#

php php8_num_str_is_numeric.php

Age must be a number

As you can see, string handling differences between PHP 7 and PHP 8 can cause applications to behave differently, with potentially disastrous results. Let's now have a look at inconsistent results involving well-formed strings.

Dealing with inconsistent string-to-numeric comparison results

To complete a non-strict comparison involving string and numeric data, the PHP engine first performs a type-juggling operation that internally converts the string to a number before performing the comparison. Even a well-formed numeric string, however, can yield results that would be viewed as nonsensical from a human perspective.

As an example, have a look at this code sample:

  1. First we perform a non-strict comparison between a variable, $zero, with a value of zero and a variable, $string, with a value of ABC:

    $zero   = 0;

    $string = 'ABC';

    $result = ($zero == $string) ? 'is' : 'is not';

    echo "The value $zero $result the same as $string "2

  2. The following non-strict comparison uses in_array() to locate a value of zero in the $array array:

    $array  = [1 => 'A', 2 => 'B', 3 => 'C'];

    $result = (in_array($zero, $array))

            ? 'is in' : 'is not in';

    echo "The value $zero $result "

         . var_export($array, TRUE)3

  3. Finally, we perform a non-strict comparison between a leading-numeric string, 42abc88, and a hardcoded number, 42:

    $mixed  = '42abc88';

    $result = ($mixed == 42) ? 'is' : 'is not';

    echo " The value $mixed $result the same as 42 ";

The results running in PHP 7 defy human comprehension! Here are the PHP 7 results:

root@php8_tips_php7 [ /repo/ch06 ]#

php php7_compare_num_str.php

The value 0 is the same as ABC

The value 0 is in

array (1 => 'A', 2 => 'B', 3 => 'C')

The value 42abc88 is the same as 42

From a human perspective, none of these results make any sense! From the computer's perspective, on the other hand, it makes perfect sense. The string ABC, when converted to a number, ends up with a value of zero. Likewise, when the array search is made, each array element, having only a string value, ends up being interpolated as zero.

The case of the leading-numeric string is a bit trickier. In PHP 7, the interpolation algorithm converts numeric characters until the first non-numeric character is encountered. Once that happens, the interpolation stops. Accordingly, the string 42abc88 becomes an integer, 42, for comparison purposes. Now let's have a look at how PHP 8 handles string-to-numeric comparisons.

Understanding comparison changes made in PHP 8

In PHP 8, if a string is compared with a number, only numeric strings are considered valid for comparison. Strings in exponential notation are also considered valid for comparison, as well as numeric strings with leading or trailing whitespace. It's extremely important to note that PHP 8 makes this determination before converting the string.

Have a look at the output of the same code example described in the previous subsection (Dealing with inconsistent string-to-numeric comparison results), running in PHP 8:

root@php8_tips_php8 [ /repo/ch06 ]#

php php7_compare_num_str.php

The value 0 is not the same as ABC

The value 0 is not in

array (1 => 'A', 2 => 'B', 3 => 'C')

The value 42abc88 is not the same as 42

So, as you can see from the output, there is a massive potential for your application to change its behavior following a PHP 8 upgrade. As a final note in PHP 8 string handling, let's look at how you can avoid upgrade issues.

Avoiding problems during a PHP 8 upgrade

The main issue you face is the difference in how PHP 8 handles non-strict comparisons that involve operands with different data types. If one operand is either int or float, and the other operand is string, you have a potential problem post-upgrade. If the string is a valid numeric string, the non-strict comparison will proceed without any issues.

The following operators are affected: <=>, ==, !=, >, >=, <, and <=. The following functions are affected if the option flags are set to default:

  • in_array()
  • array_search()
  • array_keys()
  • sort()
  • rsort()
  • asort()
  • arsort()
  • array_multisort()

    Tip

    For more information on improved numeric string handling in PHP 8, refer to the following link: https://wiki.php.net/rfc/saner-numeric-strings. A related PHP 8 change is documented here: https://wiki.php.net/rfc/string_to_number_comparison.

The best practice is to minimize PHP type juggling by providing type hints for functions or methods. You can also force the data type before the comparison. Finally, consider making use of strict comparisons, although this might not be suitable in all situations.

Now that you have an understanding of how to properly handle comparisons involving numeric strings in PHP 8, let's now have a look at PHP 8 changes involving arithmetic, bitwise, and concatenation operations.

Handling differences in arithmetic, bitwise, and concatenation operations

Arithmetic, bitwise, and concatenation operations are at the heart of any PHP application. In this section, you learn about hidden dangers that might arise in these simple operations following a PHP 8 migration. You must learn about the changes made in PHP 8 so that you can avoid a potential code break in your application. Because these operations are so ordinary, without this knowledge, you will be hard pressed to discover post-migration errors.

Let's first have a look at how PHP handles non-scalar data types in arithmetic and bitwise operations.

Handling non-scalar data types in arithmetic and bitwise operations

Historically, the PHP engine has been very forgiving about using mixed data types in an arithmetic or bitwise operation. We've already had a look at comparison operations that involve numeric, leading-numeric, and non-numeric strings and numbers. As you learned, when a non-strict comparison is used, PHP invokes type juggling to convert the string to a number before performing the comparison. A similar action takes place when PHP performs an arithmetic operation that involves numbers and strings.

Prior to PHP 8, non-scalar data types (data types other than string, int, float, or boolean) were allowed in an arithmetic operation. PHP 8 has clamped down on this bad practice, and no longer allows operands of the array, resource, or object type. PHP 8 consistently throws a TypeError when the non-scalar operands are used in an arithmetic operation. The only exception to this general change is that you can still perform arithmetic operations where all operands are of the array type.

Tip

For further information on the vital change in arithmetic and bitwise operations, have a look here: https://wiki.php.net/rfc/arithmetic_operator_type_checks.

Here is a code example to illustrate arithmetic operator handling differences in PHP 8:

  1. First, we define sample non-scalar data to test in an arithmetic operation:

    // /repo/ch06/php8_arith_non_scalar_ops.php

    $fn  = __DIR__ . '/../sample_data/gettysburg.txt';

    $fh  = fopen($fn, 'r');

    $obj = new class() { public $val = 99; };

    $arr = [1,2,3];

  2. We then attempt to add the integer 99 to a resource, object, and to perform a modulus operation on an array:

    echo "Adding 99 to a resource ";

    try { var_dump($fh + 99); }

    catch (Error $e) { echo $e . " "; }

    echo " Adding 99 to an object ";

    try { var_dump($obj + 99); }

    catch (Error $e) { echo $e . " "; }

    echo " Performing array % 99 ";

    try { var_dump($arr % 99); }

    catch (Error $e) { echo $e . " "; }

  3. Finally, we add two arrays together:

    echo " Adding two arrays ";

    try { var_dump($arr + [99]); }

    catch (Error $e) { echo $e . " "; }

When we run the code example, note how PHP 7 performs silent conversions and allows the operations to continue:

root@php8_tips_php7 [ /repo/ch06 ]#

php php8_arith_non_scalar_ops.php

Adding 99 to a resource

/repo/ch06/php8_arith_non_scalar_ops.php:10:

int(104)

Adding 99 to an object

PHP Notice:  Object of class class@anonymous could not be converted to int in /repo/ch06/php8_arith_non_scalar_ops.php on line 13

/repo/ch06/php8_arith_non_scalar_ops.php:13:

int(100)

Performing array % 99

/repo/ch06/php8_arith_non_scalar_ops.php:16:

int(1)

Adding two arrays

/repo/ch06/php8_arith_non_scalar_ops.php:19:

array(3) {

  [0] =>  int(1)

  [1] =>  int(2)

  [2] =>  int(3)

}

What is particularly astonishing is how we can perform a modulus operation against an array! When adding a value to an object, a Notice is generated in PHP 7. However, PHP type juggles the object to an integer with a value of 1, giving a result of 100 to the arithmetic operation.

The output running the same code sample in PHP 8 is quite different:

root@php8_tips_php8 [ /repo/ch06 ]#

php php8_arith_non_scalar_ops.php

Adding 99 to a resource

TypeError: Unsupported operand types: resource + int in /repo/ch06/php8_arith_non_scalar_ops.php:10

Adding 99 to an object

TypeError: Unsupported operand types: class@anonymous + int in /repo/ch06/php8_arith_non_scalar_ops.php:13

Performing array % 99

TypeError: Unsupported operand types: array % int in /repo/ch06/php8_arith_non_scalar_ops.php:16

Adding two arrays

array(3) {

  [0]=>  int(1)

  [1]=>  int(2)

  [2]=>  int(3)

}

As you can see from the output, PHP 8 consistently throws a TypeError, except when adding two arrays. In both outputs, you may observe that when adding two arrays, the second operand is ignored. If the objective is to combine the two arrays, you must use array_merge() instead.

Let's now turn our attention to a potentially significant change in PHP 8 string handling pertaining to the order of precedence.

Examining changes in the order of precedence

The order of precedence, also known as the order of operations, or operator precedence, is a mathematical concept established in the late 18th and early 19th centuries. PHP also adopted the mathematical operator precedence rules, with a unique addition: the concatenate operator. An assumption was made by the founders of the PHP language that the concatenate operator had equal precedence over the arithmetic operators. This assumption was never challenged until the arrival of PHP 8.

In PHP 8, arithmetic operations are given precedence over concatenation. The concatenate operator demotion now places it below the bit shift operators (<< and >>). There is a potential backward-compatible break in any place where you don't use parentheses to clearly define mixed arithmetic and concatenate operations.

This change, in itself, will not throw an Error or generate Warnings or Notices, and thereby presents the potential for a hidden code break.

Tip

For more information on the reasoning for this change, refer to the following link:

https://wiki.php.net/rfc/concatenation_precedence

The following example most clearly shows the effect of this change:

echo 'The sum of 2 + 2 is: ' . 2 + 2;

Here is the output of this simple statement in PHP 7:

root@php8_tips_php7 [ /repo/ch06 ]#

php -r "echo 'The sum of 2 + 2 is: ' . 2 + 2;"

PHP Warning:  A non-numeric value encountered in Command line code on line 1

2

In PHP 7, because the concatenate operator has equal precedence over the addition operator, the string The sum of 2 + 2 is: is first concatenated with the integer value 2. The new string is then type juggled to an integer, generating a Warning. The value of the new string is evaluated at 0, which is then added to integer 2, producing the output of 2.

In PHP 8, however, the addition takes place first, after which the result is concatenated with the initial string. Here is the result running in PHP 8:

root@php8_tips_php8 [ /repo/ch06 ]#

php -r "echo 'The sum of 2 + 2 is: ' . 2 + 2;"

The sum of 2 + 2 is: 4

As you can see from the output, the result is much closer to human expectations!

One more illustration should drive home the differences demoting the concatenate operator can make. Have a look at this line of code:

echo '1' . '11' + 222;

Here is the result running in PHP 7:

root@php8_tips_php7 [ /repo/ch06 ]#

php -r "echo '1' . '11' + 222;"

333

PHP 7 performs the concatenation first, producing a string, 111. This is type juggled and added to integer 222, resulting in a final value integer, 333. Here is the result running in PHP 8:

root@php8_tips_php8 [ /repo/ch06 ]#

php -r "echo '1' . '11' + 222;"

1233

In PHP 8, the second string, 11, is type juggled and added to integer 222, producing an interim value, 233. This is type juggled to a string and prepended with 1, resulting in a final string value of 1233.

Now that you are aware of changes to arithmetic, bitwise, and concatenation operations in PHP 8, let's have a look at a new trend introduced in PHP 8: locale independence.

Taking advantage of locale independence

In versions of PHP prior to PHP 8, several string functions and operations were tied to the locale. The net effect was that numbers were internally stored differently depending on the locale. This practice introduced subtle inconsistencies that were extremely difficult to detect. After reviewing the material presented in this chapter, you will be in a better position to detect potential application code changes following a PHP 8 upgrade, thereby avoiding application failure.

Understanding the problems associated with locale dependence

The unfortunate side effect of locale dependence in earlier PHP versions was inconsistent results when typecasting from float to string and then back again. Inconsistencies were also seen when a float value was concatenated to a string. Certain optimizing operations performed by OpCache resulted in the concatenation operation occurring before the locale had been set, yet another way in which inconsistent results might be produced.

In PHP 8, vulnerable operations and functions are now locale-independent. What this means is that all float values are now stored using a period as the decimal separator. The default locale is no longer inherited from the environment by default. If you need the default locale to be set, you must now explicitly call setlocale().

Reviewing functions and operations affected by locale independence

Most PHP functions are not affected by the switch to locale independence for the simple reason that locale is irrelevant to that function or extension. Furthermore, most PHP functions and extensions are already locale-independent. Examples include the PDO extension, along with functions such as var_export() and json_encode(), and the printf() family.

Functions and operations affected by locale independence include the following:

  • (string) $float
  • strval($float)
  • print_r($float)
  • var_dump($float)
  • debug_zval_dump($float)
  • settype($float, "string")
  • implode([$float])
  • xmlrpc_encode($float)

Here is a code example that illustrates handling differences due to locale independence:

  1. First, we define an array of locales to test. The locales chosen use different ways to represent the decimal portion of a number:

    // /repo/ch06/php8_locale_independent.php

    $list = ['en_GB', 'fr_FR', 'de_DE'];

    $patt = "%15s | %15s ";

  2. We then loop through the locales, set the locale, and perform a float-to-string followed by a string-to-float conversion, echoing the results at each step:

    foreach ($list as $locale) {

        setlocale(LC_ALL, $locale);

        echo "Locale          : $locale ";

        $f = 123456.789;

        echo "Original        : $f ";

        $s = (string) $f;

        echo "Float to String : $s ";

        $r = (float) $s;

        echo "String to Float : $r ";

    }

If we run this example in PHP 7, note the result:

root@php8_tips_php7 [ /repo/ch06 ]#

php php8_locale_independent.php

Locale          : en_GB

Original        : 123456.789

Float to String : 123456.789

String to Float : 123456.789

Locale          : fr_FR

Original        : 123456,789

Float to String : 123456,789

String to Float : 123456

Locale          : de_DE

Original        : 123456,789

Float to String : 123456,789

String to Float : 123456

As you can see from the output, the number is stored internally using a period for a decimal separator for en_GB, whereas the comma is used for locales fr_FR and de_DE. However, when the string is converted back to a number, the string is treated as a leading-numeric string if the decimal separator is not a period. In two of the locales, the presence of the comma stops the conversion process. The net effect is that the decimal portion is dropped and precision is lost.

The results when running the same code sample in PHP 8 are shown here:

root@php8_tips_php8 [ /repo/ch06 ]#

php php8_locale_independent.php

Locale          : en_GB

Original        : 123456.789

Float to String : 123456.789

String to Float : 123456.789

Locale          : fr_FR

Original        : 123456.789

Float to String : 123456.789

String to Float : 123456.789

Locale          : de_DE

Original        : 123456.789

Float to String : 123456.789

String to Float : 123456.789

In PHP 8, no precision is lost and the number is consistently represented using a period for the decimal separator, regardless of the locale.

Please note that you can still represent a number according to its locale by using the number_format() function, or by using the NumberFormatter class (from the Intl extension). It's interesting to note that the NumberFormatter class stores numbers internally in a locale-independent manner!

Tip

For more information, have a look at this article: https://wiki.php.net/rfc/locale_independent_float_to_string.

For more information on international number formatting, refer to the following link:https://www.php.net/manual/en/class.numberformatter.php

Now that you are aware of the locale-independent aspects present in PHP 8, we need to have a look at changes in array handling.

Handling arrays in PHP 8

Aside from improvements in performance, the two main changes in PHP 8 array handling pertain to the handling of negative offsets and curly brace ({}) usage. Since both of these changes could result in application code breaks following a PHP 8 migration, it's important to cover them here. Awareness of the issues presented here gives you a better chance to get broken code working again in short order.

Let's have a look at negative array offset handling first.

Dealing with negative offsets

When assigning a value to an array in PHP, if you do not specify an index, PHP will automatically assign one for you. The index chosen in this manner is an integer that represents a value one higher than the highest currently assigned integer key. If no integer index key has yet been assigned, the automatic index assignment algorithm starts at zero.

In PHP 7 and below, however, this algorithm is not applied consistently in the case of a negative integer index. If a numeric array started with a negative number for its index, auto-indexing jumps to zero (0) regardless of what the next number would ordinarily be. In PHP 8, on the other hand, automatic indexing consistently increments by a value of +1 regardless of whether the index is a negative or positive integer.

A possible backward-compatible code break is present if your code relies upon auto-indexing, and any of the starting indices are negative numbers. Detection of this issue is difficult as auto-indexing occurs silently, without any Warnings or Notices.

The following code example illustrates the difference in behavior between PHP 7 and PHP 8:

  1. First, we define an array with only negative integers as indexes. We use var_dump() to reveal this array:

    // /repo/ch06/php8_array_negative_index.php

    $a = [-3 => 'CCC', -2 => 'BBB', -1 => 'AAA'];

    var_dump($a);

  2. We then define a second array and initialize the first index to -3. We then add additional array elements, but without specifying an index. This causes auto-indexing to occur:

    $b[-3] = 'CCC';

    $b[] = 'BBB';

    $b[] = 'AAA';

    var_dump($b);

  3. If we then run the program in PHP 7, note that the first array is rendered correctly. It's entirely possible to have negative array indexes in PHP 7 and earlier as long as they're directly assigned. Here is the output:

    root@php8_tips_php7 [ /repo/ch06 ]#

    php php8_array_negative_index.php

    /repo/ch06/php8_array_negative_index.php:6:

    array(3) {

      [-3] =>  string(3) "CCC"

      [-2] =>  string(3) "BBB"

      [-1] =>  string(3) "AAA"

    }

    /repo/ch06/php8_array_negative_index.php:12:

    array(3) {

      [-3] =>  string(3) "CCC"

      [0] =>  string(3) "BBB"

      [1] =>  string(3) "AAA"

    }

  4. However, as you can see from the second var_dump() output, automatic array indexing skips to zero regardless of the previous high value.
  5. In PHP 8, on the other hand, you can see that the output is consistent. Here's the PHP 8 output:

    root@php8_tips_php8 [ /repo/ch06 ]#

    php php8_array_negative_index.php

    array(3) {

      [-3]=>  string(3) "CCC"

      [-2]=>  string(3) "BBB"

      [-1]=>  string(3) "AAA"

    }

    array(3) {

      [-3]=>  string(3) "CCC"

      [-2]=>  string(3) "BBB"

      [-1]=>  string(3) "AAA"

    }

  6. As you can see from the output, the array indexes are automatically assigned, incremented by a value of 1, making the two arrays identical.

    Tip

    For more information on this enhancement, have a look at this article: https://wiki.php.net/rfc/negative_array_index.

Now that you are aware of the potential code break regarding auto-assignment of indexes involving negative values, let's turn our attention to the other area of interest: the use of curly braces.

Handling curly brace usage changes

Curly braces ({}) are a familiar sight for any developer creating PHP code. The PHP language, written in C, makes extensive use of C syntax, including curly braces. It is well known that curly braces are used to delineate blocks of code in control structures (for example, if {}), in loops (for example, for () {}), in functions (for example, function xyz() {}), and classes.

In this subsection, however, we will restrict our examination of curly brace usage to that associated with variables. One potentially significant change in PHP 8 is the use of curly braces to identify an array element. The use of curly braces to designate array offsets is now deprecated as of PHP 8.

The old usage has been highly contentious given the following:

  • Its use can easily be confused with the use of curly braces inside doubly quoted strings.
  • Curly braces cannot be used to make array assignments.

    Accordingly, the PHP core team needed to either make the use of curly braces consistent with square brackets ([ ]) ... or just get rid of this curly brace usage. The final decision was to remove support for curly braces with arrays.

    Tip

    For more information on the background behind the change, refer to the following link: https://wiki.php.net/rfc/deprecate_curly_braces_array_access.

Here is a code example that illustrates the point:

  1. First, we define an array of callbacks that illustrate removed or illegal curly brace usage:

    // /repo/ch06/php7_curly_brace_usage.php

    $func = [

        1 => function () {

            $a = ['A' => 111, 'B' => 222, 'C' => 333];

            echo 'WORKS: ' . $a{'C'} . " ";},

        2 => function () {

            eval('$a = {"A","B","C"};');

        },

        3 => function () {

            eval('$a = ["A","B"]; $a{} = "C";');

        }

    ];

  2. We then loop through the callbacks using a try/catch block to capture errors that are thrown:

    foreach ($func as $example => $callback) {

        try {

            echo " Testing Example $example ";

            $callback();

        } catch (Throwable $t) {

            echo $t->getMessage() . " ";

        }

    }

If we run the example in PHP 7, the first callback works. The second and third cause a ParseError to be thrown:

root@php8_tips_php7 [ /repo/ch06 ]#

php php7_curly_brace_usage.php

Testing Example 1

WORKS: 333

Testing Example 2

syntax error, unexpected '{'

Testing Example 3

syntax error, unexpected '}'

When we run the same example in PHP 8, however, none of the examples work. Here is the PHP 8 output:

root@php8_tips_php8 [ /repo/ch06 ]#

php php7_curly_brace_usage.php

PHP Fatal error:  Array and string offset access syntax with curly braces is no longer supported in /repo/ch06/php7_curly_brace_usage.php on line 8

This potential code break is easy to detect. However, because your code has many curly braces, you might have to wait for the fatal Error to be thrown to capture the code break.

Now that you have an idea of changes in array handling in PHP 8, let's have a look at changes in security-related functions.

Mastering changes in security functions and settings

Any changes to PHP security features are worth noting. Unfortunately, given the state of the world today, attacks on any web-facing code are a given. Accordingly, in this section, we address several changes to security-related PHP functions in PHP 8. The changed functions affected include the following:

  • assert()
  • password_hash()
  • crypt()

In addition, there was a change in how PHP 8 treats any functions defined in the php.ini file using the disable_functions directive. Let's have a look at this directive to begin with.

Understanding changes in disabled functions handling

Web hosting companies often offer heavily discounted shared hosting packages. Once a customer signs up, the IT staff at the hosting company creates an account on the shared server, assigns a disk quota to control disk space usage, and creates a virtual host definition on the web service. The problem such hosting companies face, however, is that allowing unrestricted access to PHP poses a security risk to both the shared hosting company as well as other users on the same server.

To address this issue, IT staff often assign a comma-separated list of functions to the php.ini directive, disable_functions. In so doing, any function on this list cannot be used in PHP code running on that server. Functions that typically end up on this list are those that allow operating system access, such as system() or shell_exec().

Only internal PHP functions can end up on this list. Internal functions are those included in the PHP core as well as functions provided via extensions. User-defined functions are not affected by this directive.

Examining disabled functions' handling differences

In PHP 7 and earlier, disabled functions could not be re-defined. In PHP 8, disabled functions are treated as if they never existed, which means re-definition is possible.

Important note

Just because you can redefine the disabled function in PHP 8 does not mean that the original functionality has been restored!

To illustrate this concept, we first add this line to the php.ini file:disable_functions=system.

Note that we need to add this to both Docker containers (both PHP 7 and PHP 8) in order to complete the illustration. The commands to update the php.ini files are shown here:

root@php8_tips_php7 [ /repo/ch06 ]#

echo "disable_functions=system">>/etc/php.ini

root@php8_tips_php8 [ /repo/ch06 ]#

echo "disable_functions=system">>/etc/php.ini

If we then attempt to use the system() function, the attempt fails in both PHP 7 and PHP 8. Here, we show the output from PHP 8:

root@php8_tips_php8 [ /repo/ch06 ]#

php -r "system('ls -l');"

PHP Fatal error:  Uncaught Error: Call to undefined function system() in Command line code:1

We then define some program code that redefines the banned function:

// /repo/ch06/php8_disabled_funcs_redefine.php

function system(string $cmd, string $path = NULL) {

    $output = '';

    $path = $path ?? __DIR__;

    if ($cmd === 'ls -l') {

        $iter = new RecursiveDirectoryIterator($path);

        foreach ($iter as $fn => $obj)

            $output .= $fn . " ";

    }

    return $output;

}

echo system('ls -l');

As you can see from the code example, we've created a function that mimics the behavior of an ls -l Linux system call, but only uses safe PHP functions and classes. If we try to run this in PHP 7, however, a fatal Error is thrown. Here is the PHP 7 output:

root@php8_tips_php7 [ /repo/ch06 ]#

php php8_disabled_funcs_redefine.php

PHP Fatal error:  Cannot redeclare system() in /repo/ch06/php8_disabled_funcs_redefine.php on line 17

In PHP 8, however, our function redefinition succeeds, as shown here:

root@php8_tips_php8 [ /repo/ch06 ]#

php php8_disabled_funcs_redefine.php

/repo/ch06/php8_printf_vs_vprintf.php

/repo/ch06/php8_num_str_non_wf_extracted.php

/repo/ch06/php8_vprintf_bc_break.php

/repo/ch06/php7_vprintf_bc_break.php

... not all output is shown ...

/repo/ch06/php7_curly_brace_usage.php

/repo/ch06/php7_compare_num_str_valid.php

/repo/ch06/php8_compare_num_str.php

/repo/ch06/php8_disabled_funcs_redefine.php

You now have an idea of how to work with disabled functions. Next, let's have a look at changes to the vital crypt() function.

Learning about changes to the crypt() function

The crypt() function has been a staple of PHP hash generation since PHP version 4. One of the reasons for its resilience is because it has so many options. If your code uses crypt() directly, you'll be pleased to note that if an unusable salt value is provided, Defense Encryption Standard (DES), long considered broken, is no longer the fallback in PHP 8! The salt is also sometimes referred to as the initialization vector (IV).

Another important change involves the rounds value. A round is like shuffling a deck of cards: the more times you shuffle, the higher the degree of randomization (unless you're dealing with a Las Vegas card shark!). In cryptography, blocks are analogous to cards. During each round, a cryptographic function is applied to each block. If the cryptographic function is simple, the hash can be generated more quickly; however, a larger number of rounds are required to fully randomize the blocks.

The SHA-1 (Secure Hash Algorithm) family uses a fast but simple algorithm, and thus requires more rounds. The SHA-2 family, on the other hand, uses a more complex hashing function, which takes more resources, but fewer rounds.

When using the PHP crypt() function in conjunction with CRYPT_SHA256, (SHA-2 family), PHP 8 will no longer silently resolve the rounds parameter to the closest limit. Instead, crypt() will fail with a *0 return, matching glibc behavior. In addition, in PHP 8, the second argument (the salt), is now mandatory.

The following example illustrates the differences between PHP 7 and PHP 8 when using the crypt() function:

  1. First, we define variables representing an unusable salt value, and an illegal number of rounds:

    // /repo/ch06/php8_crypt_sha256.php

    $password = 'password';

    $salt     = str_repeat('+x=', CRYPT_SALT_LENGTH + 1);

    $rounds   = 1;

  2. We then create two hashes using the crypt() function. In the first usage, $default is the result after supplying an invalid salt argument. The second usage, $sha256, provides a valid salt value, but an invalid number of rounds:

    $default  = crypt($password, $salt);

    $sha256   = crypt($password,

        '$5$rounds=' . $rounds . '$' . $salt . '$');

    echo "Default : $default ";

    echo "SHA-256 : $sha256 ";

Here is the output of the code example running in PHP 7:

root@php8_tips_php7 [ /repo/ch06 ]#

php php8_crypt_sha256.php

PHP Deprecated:  crypt(): Supplied salt is not valid for DES. Possible bug in provided salt format. in /repo/ch06/php8_crypt_sha256.php on line 7

Default : +xj31ZMTZzkVA

SHA-256 : $5$rounds=1000$+x=+x=+x=+x=+x=+

$3Si/vFn6/xmdTdyleJl7Rb9Heg6DWgkRVKS9T0ZZy/B

Notice how PHP 7 silently modifies the original request. In the first case, crypt() falls back to DES (!). In the second case, PHP 7 silently alters the rounds value from 1 to the nearest limit of 1000.

The same code running in PHP 8, on the other hand, fails and returns *0, as shown here:

root@php8_tips_php8 [ /repo/ch06 ]#

php php8_crypt_sha256.php

Default : *0

SHA-256 : *0

As we have stressed repeatedly in this book, when PHP makes assumptions for you, ultimately you end up with bad code that produces inconsistent results. In the code example just shown, the best practice would be to define a class method or function that exerts greater control over its parameters. In this manner, you can validate the parameters and avoid having to rely upon PHP assumptions.

Next, we take a look at changes to the password_hash() function.

Dealing with changes to password_hash()

Over the years, so many developers had misused crypt() that the PHP core team decided to add a wrapper function, password_hash(). This proved to be a smashing success and is now one of the most widely used security functions. Here is the function signature for password_hash():

password_hash(string $password, mixed $algo, array $options=?)

Algorithms currently supported include bcrypt, Argon2i, and Argon2id. It's recommended that you use the predefined constants for algorithms: PASSWORD_BCRYPT, PASSWORD_ARGON2I, and PASSWORD_ARGON2ID. The PASSWORD_DEFAULT algorithm is currently set to bcrypt. Options vary according to the algorithm. If you use either PASSWORD_BCRYPT or the PASSWORD_DEFAULT algorithms, the options include cost and salt.

Conventional wisdom suggests that it's better to use the randomly generated salt created by the password_hash() function. In PHP 7, the salt option was deprecated and is now ignored in PHP 8. This won't cause a backward-compatible break unless you're relying on salt for some other reason.

In this code example, a non-random salt value is used:

// /repo/ch06/php8_password_hash.php

$salt = 'xxxxxxxxxxxxxxxxxxxxxx';

$password = 'password';

$hash = password_hash(

    $password, PASSWORD_DEFAULT, ['salt' => $salt]);

echo $hash . " ";

var_dump(password_get_info($hash));

In the PHP 7 output, a deprecation Notice is issued:

root@php8_tips_php7 [ /repo/ch06 ]# php php8_password_hash.php PHP Deprecated:  password_hash(): Use of the 'salt' option to password_hash is deprecated in /repo/ch06/php8_password_hash.php on line 6

$2y$10$xxxxxxxxxxxxxxxxxxxxxuOd9YtxiLKHM/l98x//sqUV1V2XTZEZ.

/repo/ch06/php8_password_hash.php:8:

array(3) {

  'algo' =>  int(1)

  'algoName' =>  string(6) "bcrypt"

  'options' =>   array(1) { 'cost' => int(10) }

}

You'll also note from the PHP 7 output that the non-random salt value is clearly visible. One other thing to note is that when password_get_info() is executed, the algo key shows an integer value that corresponds to one of the predefined algorithm constants.

The PHP 8 output is somewhat different, as seen here:

root@php8_tips_php8 [ /repo/ch06 ]# php php8_password_hash.php PHP Warning:  password_hash(): The "salt" option has been ignored, since providing a custom salt is no longer supported in /repo/ch06/php8_password_hash.php on line 6

$2y$10$HQNRjL.kCkXaR1ZAOFI3TuBJd11k4YCRWmtrI1B7ZDaX1Jngh9UNW

array(3) {

  ["algo"]=>  string(2) "2y"

  ["algoName"]=>  string(6) "bcrypt"

  ["options"]=>  array(1) { ["cost"]=> int(10) }

}

You can see that the salt value was ignored, and a random salt used instead. Instead of a Notice, PHP 8 issues a Warning regarding the use of the salt option. Another point to note from the output is that when password_get_info() is called, the algorithm key returns a string rather than an integer in PHP 8. This is because the predefined algorithm constants are now string values that correspond to their signature when used in the crypt() function.

The last function we will examine, in the next subsection, is assert().

Learning about changes to assert()

The assert() function is normally associated with testing and diagnostics. We include it in this subsection, as it often has security implications. Developers sometimes use this function when attempting to trace potential security vulnerabilities.

To use the assert() function, you must first enable it by adding a php.ini file setting zend.assertions=1. Once enabled, you can place one or more assert() function calls at any place within your application code.

Understanding changes to assert() usage

As of PHP 8, it's no longer possible to present assert() with string arguments to be evaluated: instead, you must provide an expression. This presents a potential code break because in PHP 8, the string is treated as an expression, and therefore always resolves to the Boolean TRUE. Also, both the assert.quiet_eval php.ini directive, and the ASSERT_QUIET_EVAL pre-defined constant used with assert_options(), have been removed in PHP 8 as they now have no effect.

To illustrate the potential problem, we first activate assertions by setting the php.ini directive, zend.assertions=1. We then define an example program as follows:

  1. We use ini_set() to cause assert() to throw an exception. We also define a variable, $pi:

    // /repo/ch06/php8_assert.php

    ini_set('assert.exception', 1);

    $pi = 22/7;

    echo 'Value of 22/7: ' . $pi . " ";

    echo 'Value of M_PI: ' . M_PI . " ";

  2. We then attempt an assertion as an expression, $pi === M_PI:

    try {

        $line    = __LINE__ + 2;

        $message = "Assertion expression failed ${line} ";

        $result  = assert($pi === M_PI,

            new AssertionError($message));

        echo ($result) ? "Everything's OK "

                       : "We have a problem ";

    } catch (Throwable $t) {

        echo $t->getMessage() . " ";

    }

  3. In the last try/catch block, we attempt an assertion as a string:

    try {

        $line    = __LINE__ + 2;

        $message = "Assertion string failed ${line} ";

        $result  = assert('$pi === M_PI',

            new AssertionError($message));

        echo ($result) ? "Everything's OK "

                       : "We have a problem ";

    } catch (Throwable $t) {

        echo $t->getMessage() . " ";

    }

  4. When we run the program in PHP 7, everything works as expected:

    root@php8_tips_php7 [ /repo/ch06 ]# php php8_assert.php

    Value of 22/7: 3.1428571428571

    Value of M_PI: 3.1415926535898

    Assertion as expression failed on line 18

    Assertion as a string failed on line 28

  5. The value of M_PI comes from the math extension, and is far more accurate than simply dividing 22 by 7! Accordingly, both assertions throw an exception. In PHP 8, however, the output is significantly different:

    root@php8_tips_php8 [ /repo/ch06 ]# php php8_assert.php

    Value of 22/7: 3.1428571428571

    Value of M_PI: 3.1415926535898

    Assertion as expression failed on line 18

    Everything's OK

The assertion as a string is interpreted as an expression. Because the string is not empty, the Boolean result is TRUE, returning a false positive. If your code relies upon the result of an assertion as a string, it is bound to fail. As you can see from the PHP 8 output, however, an assertion as an expression works the same in PHP 8 as in PHP 7.

Tip

Best practice: Do not use assert() in production code. If you do use assert(), always provide an expression, not a string.

Now that you have an idea of changes to security-related functions, we bring this chapter to a close.

Summary

In this chapter, you learned about differences in string handling between PHP 8 and earlier versions, and how to develop workarounds that address differences in string handling. As you learned, PHP 8 exerts greater control over the data types of string function arguments, as well as introducing consistency in what happens if an argument is missing or null. As you learned, a big problem with earlier versions of PHP is that several assumptions were silently made on your behalf, resulting in a huge potential for unexpected results.

In this chapter, we also highlighted issues involving comparisons between numeric strings and numeric data. You learned not only about numeric strings, type-juggling, and non-strict comparisons, but also how PHP 8 corrects flaws inherent in numeric string handling that were present in earlier versions. Another topic covered in this chapter demonstrated potential issues having to do with how several operators behave differently in PHP 8. You learned how to spot potential problems and were given best practices to improve the resilience of your code.

This chapter also addressed how a number of PHP functions retained dependence upon the locale setting, and how this problem has been addressed in PHP 8. You learned that in PHP 8, floating-point representations are now uniform and no longer dependent upon the locale. You also learned about changes in how PHP 8 addresses array elements as well as changes in several security-related functions.

The tips, tricks, and techniques covered in this chapter raise awareness of inconsistent behavior in earlier versions of PHP. With this new awareness, you are in a better position to gain greater control over the use of PHP code. You are also now in a better position to detect situations that could lead to potential code breaks following a PHP 8 migration, giving you an advantage over other developers, and ultimately leading you to write PHP code that performs reliably and consistently.

The next chapter shows you how to avoid potential code breaks involving changes to PHP extensions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.184.214