Institute of Science and Technology

a center of excellence for education
Institute of Science and Technology

a center of excellence for education

PHP 8.1: A look at the new features and changes

Last year the new major version of PHP was released and although it brought many great new features, it was not very well received by the PHP developer world. Version 8.0 did not manage to convince developers to switch to a new version. Maybe PHP 8.1 will make it so – let us take a look at what’s new.

In this blog post we will cover the new features of PHP 8.1 that have already been specified as “implemented” in the RFCs. However, it is possible that the implementation may differ shortly before release.

DEPRECATE WARNINGS

For historical reasons, internal PHP functions allow a null as an argument even though the type of the argument is not nullable. There are some “polyfills”, i.e. functions that emulate PHP internal functions, to simplify the transition to new versions. Currently, these cannot correctly reproduce some PHP internal functions. [2] This is because even written functions may only receive a null as a parameter for scalar types if the parameter was also specified as nullable. The same behaviour is applied to internal functions in PHP 8.1, but with a deprecate warning initially. With PHP versions greater than 9.0, it is planned to generate a TypError for these functions. So, if you simply pass null as a parameter to the internal functions, you could have problems with the code later on.

NEW FEATURE: ARRAY_IS_LIST()

In PHP, complex structures can be mapped very easily with an array. We can simply add new array keys during runtime and change existing keys. Of course, this flexibility brings with it performance problems. A PHP array can be well-optimised if the array keys are numbered from 0 to the number of elements. This optimisation is currently difficult to implement because checking the keys is already time-consuming. The current polyfill for the function looks like this:

Listing 1
1
2
3
4
5
6
7
8
function array_is_list(array $array): bool {
    $expectedKey = 0;
    foreach ($array as $i => $_) {
        if ($i !== $expectedKey) { return false; }
        $expectedKey++;
    }
    return true;
}

With the native implementation of the function [3], the code could be implemented even more performantly because you no longer need to assume that the array keys will have a non-numerical value. This will make PHP even more performant in the future. Maybe we could think about a new data type list and treat it separately. Maybe this will even become an RFC.

NEW FEATURE: FSYNC()

In PHP, the function fflush () has been around for a long time. [4] This function forces the entire output buffer to be written into the file pointer that was passed as a parameter. However, this function only receives a true or a false from the operating system on which PHP is running. The operating system itself still must transfer the contents to the file system and this could cause problems that PHP itself is not aware of. With the function fsync () you should find out from the script whether the output buffer was also written persistently, and only then do you receive a true on success. [5]

In addition, another function fdatasync() is to be added, which ensures that data is saved like fsync but not the metadata, which may not be relevant in some cases and could then be ignored. Under Windows, the function fdatasync should be an alias for fsync because writing the buffer without the metadata is not supported under Windows.

PROHIBIT $GLOBALS AS REFERENCE

Global variables have existed in PHP from the beginning and offer a lot of flexibility. However, this flexibility often leads to page effects. The RFC is intended to prohibit assigning $GLOBALS variables as a reference to another variable. [6] To understand the RFC, one must first look at the following example.

1
2
3
$a = 1;
$GLOBALS['a'] = 2;
var_dump($a); // int(2)

Here, the variable $a is stored internally as a compiled variable so that variables can be accessed more quickly. To be able to modify the variable $a via globals, the array keys of globals are stored in such a way that the keys have a pointer to the variables. $GLOBALS array is, however, excluded from the normal array functionalities. It is possible to assign $GLOBALS to an array and that array will then have a pointer to $GLOBALS and so the new array can modify existing variables. As shown in the code below.

1
2
3
4
$a = 1;
$globals = $GLOBALS; // Ostensibly by-value copy
$globals['a'] = 2;
var_dump($a); // int(2)

This should be prevented in the future and this code would throw an error exception. The $GLOBALS array should behave like a normal array.

EXPLICIT OCTAL INTEGER NOTATION

A lot has changed in PHP when it comes to comparing strings with integers. Especially since PHP 8, the comparisons have become more explicit, leading to other problems. There are numbers that contain letters, hexadecimal or binary. For example, you can assign a binary value to a variable with $x = 0b1101; or with $y = 0xBF12; for octal values there is the possibility of simply starting with a 0, for example $z = 016; however, there are cases where you accidentally or through type casting get an octal value from an integer. For this reason, in PHP 8 there is a 0o or 0O prefix to explicitly define a value as octal. [7]

1
$octal = 0o16 == 14; //this will output a true

MYSQLI DEFAULT ERROR MODE

This new feature could lead to problems in the existing code – you need to be careful here. Since PHP 8 changed the PDO error mode to exception, all error messages that come from a database are output directly unless you intercept them. [8] The same behaviour would now like to be transferred to MySQLi, because there it is currently the same as with the old PDO. Errors are not caught, and the code continues to be executed. From PHP 8 the myqli_report should be set to MYSQLI_REPORT_ERROR and MYSQLI_REPORT_STRICT. [9]

As of PHP 8, an exception will be thrown, and thus existing code that relied on SILENT mode could now throw errors. If you still want to suppress the errors on your system, you must now use the function mysqli_report with the parameter MYSQLI_REPORT_OFF.

ENUMERATIONS

A new language construct is introduced in version 8.1: enum. [10] So what are enumerations? Again and again, you will to offer a certain number of options in the code and these are not always easy to validate or query. A typical example in a web application would be salutation or gender. You only want to offer certain options and only these are allowed. In a database you can already define an ENUM field, but in PHP, every developer takes care of this themselves. The difficult part is querying in code, every time you get a value, you have to read the complete list and then check it against.

1
2
3
4
5
$formofaddress = ['Mr.','Mrs.','Not specified'];
     
function FormOfAddressIsValid($formofaddress,$address){
return in_array($formofaddress,$address);
}

You are forced to carry this logic with you all the time and validate it everywhere you work with the other one. With PHP 8.1 it is now possible to define the following construct:

1
2
3
4
5
enum Other{
   case Mr;
   case Mrs;
   case NS;
}

With this construct we are now able to define typehints.

1
Public function setFormOfAddress(Other $other)

As a parameter, you can now pass the method Other::Mr. or Other::Mrs. It is important to know that each case in an enum is its own internal instance and therefore, you cannot compare the cases with each other.

1
2
3
$a = Other::Mr.;
$b = Other::Mrs.;
$a === $b; // false

The enum can be checked with an instanceof and if you want a value of the enum, you must use the property name to get the string.

1
Echo Other::Mr.→name; // “Mr.” as output

Now the question arises as to how it would look in a project; there are enums that have a different value in a database than the identifier. You can assign a value to a case with the following construct:

1
2
3
4
5
enum Other{
   case Mr. = ‘1’;
   case Mrs. = ‘2’;
   case NS = ‘0’;
}

This construction is also called a “Backed Enum” while one without values is a “Pure Enum”. The values of the cases may only have a scalar type and this would be an integer or string and may not have any union types etc. If a case gets a value, then all other cases must also get a value, cases are not automatically filled with values. To be able to access the value of the case there is a property value:

1
echo Other::Mr.→value; // will output “1”

Because the value is a read only property, you must not form references from the value.

1
&Other::Mr→value;

would produce an error message. Backed Enums” implement a BackedEnumInterface and have two further methods.

1
from(int|string): self

This allows you to read a case from the enum with a string to convert a value from the database into an enum, for example.

1
2
$row = $statement→fetch();
$other = Other::from($row['other']);

If the value is not a part of the enum, a ValueError error message is output. If you want to have a nullable value, you can use the other method.

1
tryFrom(int|string): ?self

The tryFrom method works like the from method, but it does not immediately return an error message but only a null. Both methods expect an int or string as a parameter, but if the strict_type is not declared in the file, you can pass other scalar types like float.  So, you could use the tryFrom method for a fallback value:

1
2
$row = $statement→fetch();
$other = Other::tryFrom($row['other'])?? Other::NS;

An enum can also contain methods and implement interfaces. The syntax for this is as follows:

Listing 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface SpecialInterface {
  public function isSpecia(): bool;
}
enum OrderStatus implements SpecialInterface {
    case ARCHIVED = 1;
    case CANCELED = 2;
   case ORDERED = 3;
   case PAYED = 4;
   case DELIVERED = 5;
 
   public function isSpecial(){
     return match($this) {
     OrderStatus::ARCHIVED, OrderStatus::CANCELED => true,
     OrderStatus::ORDERED, OrderStatus:: PAYED, OrderStatus::DELIVERED => false,
     }
   }
}
$currentStatus = OrderStatus::from(5); //delivered is read out
var_dump($currenStatus→isSpecial()); //false as output

Within a method there is the variable $this, which is available for each individual case.  Of course, private and protected methods are also allowed, but private and protected are the same because enums cannot be derived. In addition, static methods are also allowed. Here you no longer have $this available but static::. In addition, you can also define constants and the value of the constant may also be an enum.

1
2
3
4
5
6
7
enum Size {
  case Small;
  case Medium;
  case Large;
  
  public const Huge = self::Large;
}

However, there are some problems with such an assignment that you should be aware of. For example, an enum can implement an array, access interface and individual cases can be an array, but then you cannot assign the enum to a constant.

Listing 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// This is an entirely legal Enum definition.
enum Direction implements ArrayAccess {
  case Up;
  case Down;
  
  public function offsetGet($val) { ... }
  public function offsetExists($val) { ... }
  public function offsetSet($val) { throw new Exception(); }
  public functiond offsetUnset($val) { throw new Exception(); }
}
  
class Foo {
  // This is allowed.
  const Bar = Direction::Down;
  
  // This is disallowed, as it may not be deterministic.
  const Bar = Direction::Up['short'];
  // Fatal error: Cannot use [] on enums in constant expression
}
  
// This is entirely legal, because it's not a constant expression.
$x = Direction::Up['short'];

As you can see in the example above, it is allowed to implement an ArrayAccess interface, but you are not allowed to assign it to a constant if you use an array key. However, the complete case can be assigned. There are only a few differences between an enum and a normal object; enums have no state and may not be derived, so you could consider an enum as a kind of final static class. Since an enum has no state, you cannot initialise an enum with a new keyword or create a new instance via a ReflectionClass. Enums implement a UnitEnum interface and this interface automatically brings the cases() method with it. This method can be used to output all the options that an enum contains as an array. Enum cases cannot be used as array keys, but you can use them as keys in a SplObjectStorage or WeakMap. Since an enum is a singleton and is never deleted via the garbage collector, it makes no difference whether you use SplObjectStorage or WeakMap.

Of course, the appropriate reflection classes are added to the enums to be able to display an enum in detail. There will be the class ReflectionEnum, in which you can read the cases and see if an enum is a “Backed Enum” or a “Pure Enum”. For individual cases there is also a reflection class, the “ReflectionEnumUnitCase”, and for Backed Enum Cases there will be an extra “ReflectionEnumBackedCase” to be able to read out the assigned value of the case with getBackingValue(). I want to show an example of how you could use an enum:

Listing 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum QueueJobStatus: int{
 case STOPPED = -1;
 case WAITING = 0;
 case RUNNING = 1;
 case DONE = 2;
 case FAILED = 3;
 
 public function label(): string{
    return match($this) {
    static::ONHOLD => 'onhold',
   static::WAIITING => 'waiting',
   static::RUNNING => 'running',
   static::DONE => 'done',
   static::FAILED => 'failed'
   }
 }
}
 
$dbStatus = QueueJobStatus::from($row['status’]);
 
foreach(QueueJobStatus::cases() as $status){
$selected = $status === $dbStatus?’ selected’:’’;
echo sprintf(‘<option vaue=”%s”%s>%s</option>’,$case→value,$selected,$case→label());
}

CONCLUSION

At the moment, it is not clear if generics will be implemented in PHP 8.1 or if we will see them in later versions. But the enums look very promising and it is an important tool for the PHP programming language.