PHP 8.0: What's New and Changed
PHP 8.0 is a major version update and a remarkable milestone in PHP, as it brings several new features to type system, syntax, error handling, strings, object-oriented programming, and more.
It is the efforts of hundreds of people coming together to shape the future of a programming language that powers a significant portion of the Internet web sites and applications.
PHP tries to be conservative with changes that can break a majority of the applications, and yet, it brings several new major features to PHP 8.0.
Features such as Named Parameters, JIT, Attributes, and Constructor Properties bring major improvements and syntax changes, while several minor improvements such as resource
to object migrations, improved error handling, and changes and improvements in operators and engine comparisons help seamlessly reduce the chances of overlooked bugs.
Thank you for all the contributors, of whom there are hundreds, for all your efforts, in PHP core, documentation, libraries, testing tools, extensions, authors, package maintainers, PHP developers, and everyone else including you ❤.
- Major New Features
- New Functions and Classes
- Type System Improvements
- Error Handling Improvements
- Resource to Object Migration
- PHP Object Oriented Programming Changes
- String-related changes
Major New Features
Named Parameters
PHP 8.0 allows named parameters in function/method calls in addition to traditional positional parameters.
str_contains(needle: 'Bar', haystack: 'Foobar');
This makes the function/method parameter names part of the public API. The non-standardized DocBlock @no-named-arguments
expresses that the library does not provide backwards-compatibility for named parameters.
Attributes
Attributes allows declaring meta-data for functions, classes, properties, and parameters. Attributes map to PHP class names (declared with an Attribute itself), and they can be fetched programmatically with PHP Reflection API.
#[CustomAttribute]
class Foo {
#[AnotherAttribute(42)]
public function bar(): void {}
}
Attributes makes it easy and performant to declare Attributes/annotations that previously required storing them in DocBlock comments, and parsing the string to infer them.
Constructor Properties
A new syntax to declare class properties right from the class constructor (__construct
magic method).
class User {
public function __construct(private string $name) {}
}
In the constructor, PHP 8.0 supports declaring the visibility (public
, private
, or protected
) and type. Those properties will be registered as class properties with same visibility and type they are declared in the constructor.
This backwards-incompatible feature can help reduce boilerplate code when declaring value-object classes.
Just-In-Time Compilation
PHP Opcache supports JIT. It's disabled by default, and if enabled, JIT compiles and caches native instructions. It does not make a noticeable difference in IO-bound web applications, but provides a performance boost for CPU-heavy applications.
# Enabling JIT in php.ini
opcache.enable=1
opcache.jit_buffer_size=100M
opcache.jit=tracing
Note that JIT is still new, and had bug fixes as late as a day before PHP 8.0.0 release. It makes debugging and profiling more difficult with the added extra layer.
Union Types
Union Types extend type declarations (return types, parameters, and class properties) to declare more than one type.
function parse_value(string|int|float): string|null {}
It also supports false
as a special type (for Boolean false
), a trait that's prevalent in legacy code that did not use Exceptions.
Null-safe Operator
Null-safe operator provides safety in method/property chaining when the return value or property can be null
.
return $user->getAddress()?->getCountry()?->isoCode;
The ?->
null-safe operator short-circuits the rest of the expression if it encounters a null
value, and immediately returns null
without causing any errors.
match
expressions
Match expressions are similar to switch
blocks, but match
blocks provide type-safe comparisons, supports a return value, does not require break
statements to break-out, and supports multiple matching values. it also guarantees that at least one branch is matched, ensuring all cases are accounted for.
$response = match('test') {
'test' => $this->sendTestAlert(),
'send' => $this->sendNuclearAlert(),
};
Not all switch
blocks might convert well to match
blocks. Code that requires backwards-compatibility, switch
blocks with multiple statements (as opposed to single-line expressions), or expects fall-through functionality still fits the switch
statements.
WeakMaps
A WeakMap allows to store and associate arbitrary values for object keys, but without preventing the garbage collector from clearing it the object falls out of scope in everywhere else.
A WeakMap
is similar to SplObjectStorage
, as in both WeakMap
and splObjectStorage
use objects
s as the key, and allows storage of arbitrary values. However, a WeakMap
does not prevent the object from being garbage collected.
New Functions and Classes
PHP 8.0 introduces a few new functions to ease string inspections (contains, starts with substring, or ends with substring) to replace the meticulous strpos() !== false
calls that are less readable, and error-prone due to weak type comparisons.
Function | Description | Example |
---|---|---|
str_contains |
Haystack contains the needle somewhere | str_contains('Foobar', 'Foo') |
str_starts_with |
Needle starts with haystack string | str_starts_with('PHP 8.0', 'PHP) |
str_ends_with |
Needle ends with haystack string | str_ends_with('PHP 8.0', '8.0) |
PHP 8.0 also brings functions such as fdiv
, get_resource_id
, get_debug_type
, and preg_last_error_msg
Function | Description |
---|---|
fdiv |
Float division supporting IEEE-754 standard on Floating-Point Arithmetic. |
get_resource_id |
Returns the internal ID for a given PHP resource |
get_debug_type |
Returns the internal type of a passed variable |
preg_last_error_msg |
Returns a human-friendly error message for the last preg operation. |
New Stringable
interface
The new Stringable
interface is automatically added to all classes that implement __toString
method, and those explicitly declare that they implements Stringable
.
With the Stringable
interface, it is now easy to declare types as string|Stringable
for on functions that can accept/return strings or objects with a __toString()
method.
New PhpToken
Tokenizer class
The new PhpToken
class provides a more fluent Object-Oriented interface as an alternative to the legacy array-based to token_get_all
function.
Type System Improvements
PHP 8.0 improves typing system with the addition of Union Types and the mixed
type.
Union Types
Union Types extend type declarations (return types, parameters, and class properties) to declare more than one type.
function parse_value(string|int|float): string|null {}
It also supports false
as a special type (for Boolean false
), a trait that's prevalent in legacy code that did not use Exceptions.
New mixed
pseudo type
PHP 8.0 brings mixed
type, that was already being widely used in DocBlock comments.
function dd(mixed $var): void {
var_dump($var);
}
mixed
type can be used to indicate that it accepts any type, or can return any type. In a class/interface context, mixed
type plays by the same rules of Liskov Substitution Principle.
static
return type for class methods
static
return type, already support as a DocBlock return type, is now supported in PHP 8.0. The static
return type declares an object of the called class will be returned.
class Foo {
public static function getInstance(): static {
return new static();
}
}
Error Handling Improvements
A major and backwards-incompatible change in PHP is that internal functions now throw exceptions on type errors or value errors.
This corrects PHP's historical behavior of emitting a warning, and returning null
when it encounters a value that it cannot use. This behavior is often undesired because PHP warnings does not halt the execution of the remaining block.
Internal function warnings now throw TypeError
and ValueError
exceptions
Almost entirety of PHP internal functions now enforce type checking, and instead of warnings, PHP now throws TypeError
or ValueError exceptions. On legacy code bases, this change can cause issues, now that the errors are more boldly and unforgivably handled.
throw
in expressions
Prior to PHP 8.0, it was not possible to throw
exceptions from an expression (e.g a ternary statement). This is now allowed in PHP 8.0.
$value = isset($_GET['value'])
? $_GET['value']
: throw new \InvalidArgumentException('value not set');
catch
exceptions only by type
It is possible to catch
exceptions by their type, without capturing the exception object.
try {}
catch(TypeError) {
// Did not catch the $exception object
}
Default error reporting is set to E_ALL
PHP 8.0 default configuration is to show all error messages by default. It was configured to hide deprecation and strict warnings in older versions.
PHP Startup Errors are displayed by default
PHP now displays startup-errors (failure to load dynamic extensions, invalid INI configurations, etc) by default.
Assertions throw exceptions by default
PHP assertions (assert()
) now throw exceptions on assertion failures. Prior to PHP 8.0, it required an explicit configuration that was disabled by default.
@
Error Suppression operator does not silent fatal errors
PHP 8.0 corrects the behavior of @
error suppression operator, where it silenced fatal errors, which lead to a script failure, because the @
operator does not prevent fatal errors, but rather hide the error message display.
Resource to Object Migration
One of the long-term efforts in PHP development was to move away from the resource
types. They were difficult to deal with, and even in PHP 8.0, does not offer typing support.
PHP resource
objects do not play well with garbage-collector well either, which resulted in memory leaks in resource
objects such as xml
.
In PHP 8.0, some of the most used extensions changes moves away from the traditional resource
objects to standard PHP classes.
In PHP 8.0, they are work as value-objects as opposed to fully-features classes with methods in them. Majority of these classes do not allow instantiating with the new Foo()
construct either, and must be instantiated with the existing functions that returned resource
objects in prior versions.
PHP 8.0's resource
to object migration is quite seamless, as in all functions return and accept the new objects, and behave by the same semantics of the previous resource
objects.
Extension | resource (PHP < 8.0) |
object (PHP >= 8.0) |
---|---|---|
Curl | Curl |
CurlHandle |
Curl | curl_multi |
CurlMultiHandle |
Curl | curl_share |
CurlShareHandle |
GD | gd |
GdImage |
Sockets | Socket |
Socket |
Sockets | AddressInfo |
AddressInfo |
OpenSSL | OpenSSL key |
OpenSSLAsymmetricKey |
OpenSSL | OpenSSL X.509 |
OpenSSLCertificate |
OpenSSL | OpenSSL X.509 CSR |
OpenSSLCertificateSigningRequest |
XMLWriter | xmlwriter |
XMLWriter |
XML | xml |
XMLParser |
PHP Object Oriented Programming changes
PHP 8.0 is the first major version to be strict on Liskov Substitution Principle violations. Prior to PHP 8.0, PHP was not consistent on how it handled incompatible method signatures.
In PHP 8.0, all signature mismatches, including abstract traits, result in a fatal error. Further, it enforces signatures for PHP magic methods.
Fatal errors on incompatible method signatures
PHP 8.0 throws fatal errors when Liskov Substitution Principle is not followed when classes are extended, or interfaces are implemented.
Prior to PHP 8.0, incompatible signatures only emitted a warning.
class Foo {
public function process(stdClass $item): array{}
}
class SuperFoo extends Foo{
public function process(array $items): array{}
// ^^^^^ mismatch
}
Fatal error: Declaration of SuperFoo::process(array $items): array must be compatible with Foo::process(stdClass $item): array in ... on line ...
Class magic method signatures are strictly enforced
From PHP 8.0 and later, magic methods (e.g __toString()
, __get()
, etc), if they declare types, must implement the signature PHP expects. This is to avoid the smallest chance of the user declaring a magic method that doesn't follow the semantic meaning.
class Foo {
public function __toString(): object {
}
}
Declarations like Foo::__toString(): object
was allowed in previous PHP versions, but PHP 8.0 and throws an exception if the signature does not meet the requirements.
Calling non-static class methods statically result in a fatal error
PHP 8.0 no longer allows calling class methods as a static method.
class Foo {
public function bar() {}
}
Foo::bar();
Previous versions emitted a deprecation notice, but from PHP 8.0 and later, this results in a fatal error.
Inheritance rules are not applied to private
class methods
PHP 8.0 relaxes the signature, abstract
, and static
flag enforcement for private
class methods. This change comes from the rationale that private
methods are just that: Private.
From PHP 8.0, it is now allowed for the child classes to declare abstract methods, and change static/flags for private
methods.
::class
magic constant is now supported on objects
The ::class
magic constant returns the fully-qualified class name. This was only allowed on class names (such as Foo\Bar::class
), but in PHP 8.0, the ::class
magic constant works on instantiated objects too.
String-related changes
In PHP 8.0, there are several subtle changes that might be not obvious at first, but can result in quite unexpected results.
A major difference in PHP 8.0 is that, PHP now considers there is an empty string between every character in a given string.
Prior to PHP 8.0, checking for an empty string needle (""
) was not allowed, but in PHP 8.0, PHP will happily accept it, and return that there is indeed an empty string between each character.
The multi-byte handling, or functions like strlen
still returns same values as the older versions, but all functions that check for a substring in a given string are changed.
In addition, PHP 8.0 changes how the string concatenation operator priorities, and supports new modifiers in sprintf
functions such as %h
and %H
modifiers and *
width and precision modifier.
substr
, iconv_substr
, grapheme_substr
return empty string on out-of-bound offsets
These functions clamp the needle and offset parameters to the string length, and returns an empty string instead of returning false
.
substr('FooBar', 42, 3); // "" in PHP >=8.0, false in PHP < 8.0
mb_substr('FooBar', 42, 3); // "" in PHP >=8.0, false in PHP < 8.0
iconv_substr('FooBar', 42, 3); // "" in PHP >=8.0, false in PHP < 8.0
grapheme_substr('FooBar', 42, 3); // "" in PHP >=8.0, false in PHP < 8.0
+
/-
operators take higher precedence when used with concat (.
) operator
When the mathematical + and - operators are used in the same expression with the concatenation operator (.
), the + and - operators take higher precedence. This resulted in a deprecation notice in PHP versions prior to 8.0, but now it happens silently and as per the warning.
echo 35 + 7 . '.' . 0 + 5;
// 42.5 in PHP >= 8.0
// 47 in PHP <= 8.0
Locale-independent float
to string
casting
When a float
value is coerced to string
, PHP no longer uses the locale. Prior to PHP 8.0, PHP considered the current locale (which is not thread-safe) or system locale when doing so. This resulted in inconsistent string outputs because certain locales, specially European ones, swap the thousand separator and decimal sign from the US locale.
For locale-aware float
to string
cast, the %f
modifier in printf
class of functions can be used.
In addition, the new %h
and %H
modifiers for printf
class of functions provide locale-indepdent variants of %g
and %G
modifiers.
$thanks ❤
PHP 8.0 is an amazing effort from hundreds of awesome people. It's a major milesone in PHP's history. Thank you to everyone who helped from code, to documentation, to conferences, to articles to all of the developers.
New Features
- Named Parameters
- Attributes
- Class constructor property promotion
- JIT
- New `%h` and `%H` `printf` specifiers
- New `*` precision and width modifiers in `printf`
- New `Stringable` interface
- Built-in web server supports dynamic port selection
- Stack trace as string - Parameter max length is configurable
- WeakMaps
- Null-safe operator
- `static` return type for class methods
- New `get_resource_id` function
- New `str_contains` function
- New `fdiv` function
- New `get_debug_type` function
- New `preg_last_error_msg` function
- `::class` magic constant is now supported on objects
- New `ValueError` Error Exception
- New `PhpToken` Tokenizer class
- New `str_starts_with` and `str_ends_with` functions
- New `mixed` pseudo type
- New `p` date format for UTC `Z` time zone designation
- Match Expressions
- New `DateTime/DateTimeImmutable::createFromInterface()` methods
- Union Types
Syntax/Functionality Changes
- Default error reporting is set to `E_ALL`
- Inheritance rules are not applied to `private` class methods
- Calling non-static class methods statically result in a fatal error
- Apache Handler: Module name and file path changes
- Locale-independent `float` to `string` casting
- Class magic method signatures are strictly enforced
- `substr`, `iconv_substr`, `grapheme_substr` return empty string on out-of-bound offsets
- PHP Startup Errors are displayed by default
- GD Extension: Windows DLL file name changed from `php_gd2.dll` to `php_gd.dll`
- `crypt()` function requires `$salt` parameter
- PDO: Default error mode set to exceptions
- `@` Error Suppression operator does not silent fatal errors
- Trailing commas are allowed in parameter lists and closure `use` lists
- Implicit negative array key increments do not skip negative numbers
- `XMLWriter` objects replace `xmlwriter` resources
- OpenSSL: `resource` to object migration
- `XMLParser` objects replace `xml` resources
- String to number comparisons no longer coerce string to a number
- Strict type checks on arithmetic operators
- Sorting functions maintain positions of equal value items
- Internal function warnings now throw `TypeError` and `ValueError` exceptions
- Expressions can now `throw` Exceptions
- JSON extension is always available
- `catch` exceptions only by type
- `+`/`-` operators take higher precedence when used with concat (`.`) operator
- `CurlHandle` class objects replace curl handlers
- Fatal errors on incompatible method signatures
- Disabled functions behave as if they do not exist
- `GdImage` class objects replace GD image resources
- Assertions throw exceptions by default
- Sockets extension resources (`Socket` and `AddressInfo`) are class objects
Deprecations
- PostgreSQL: Several aliased functions are deprecated
- Deprecate required parameters after optional parameters in function/method signatures
- `ReflectionParameter::getClass())`, `::isArray()`, and `::isCallable()` methods deprecated
- Disabled functions: Reflection and `get_defined_functions()` deprecations
- `libxml_disable_entity_loader` function is deprecated
Removed Features and Functionality
Named Parameters
PHP 8.0 supports optionally calling functions and class methods by specifying the parameter name, instead of calling them on the order of parameters that they are declared.
PHP, and many other programming languages, support positional parameters: The caller passes the parameters in the same order the function/method declares its parameters.
In PHP 8.0, Named Parameters support is added, which means it's now possible to call a function/method by setting the parameters by their name.
Positional Parameters
function str_contains(string $haystack, string needle): bool {}
str_contains('FooBar', 'Foo');
Named Parameters
function str_contains(string $haystack, string $needle): bool {}
str_contains(haystack: 'FooBar', needle: 'Foo');
With Named Parameters, the call-site names the parameter value, and passes it to the called function; The order of the parameters are not important, as long as all the required parameters are passed.
For example, both of these statements are correct and functionally identical:
str_contains(haystack: 'FooBar', needle: 'Foo');
str_contains(needle: 'Foo', haystack: 'FooBar');
Named parameter feature offers more readability when calling a function with parameters that do not require to have a particular order, or cannot be made more intuitive.
setcookie(
name: "Awesome",
value: 'Yes',
path: "/",
domain: "example.com",
secure: true,
httponly: true
);
The name of the parameter in declaration, without the $
sign will be the parameter name.
Optional Parameters and Default Values
When a function/method declares a parameter with a default value, and if the named parameter call-site does not set a value for that parameter, the default value will be used, just like a positional parameter call.
function greet(string $name, string $greeting = 'Hello') {
echo "$greeting, $name";
}
greet('Simon'); //"Hello, Simon"
greet(name: 'Simon'); // "Hello, Simon"
Skipping Parameters
Named parameters inherit the default values if not explicitly set at the call-site. This makes it possible to skip optional parameters for a function/method call, which was not possible with positional parameters without assuming the default values.
function greet(string $name, string $greeting = 'Hello', bool $warm = false) {
echo "$greeting, $name";
if ($warm) {
echo "You are awesome";
}
}
With positional parameters, it is not possible to set a value for the third $warm
parameter without assuming the default value for the second $greeting
parameter.
greet('Simon', 'Hello', true);
With named parameters, the default value is inherited if it is not set at the call-site:
greet(name: "Simon", warm: true);
Named parameters require all non-optional parameters to be passed. If there are not enough parameters, an `ArgumentCountError exception will be thrown, just like it does with positional parameters.
str_replace(search: 'foo');
Fatal error: Uncaught ArgumentCountError: str_replace() expects at least 3 arguments, 1 given in ...
Unknown Parameters
It is not allowed to pass additional parameters or unknown parameters in a named parameter call-site, and will result in an error:
str_replace(time_limit: "mi");
Fatal error: Uncaught Error: Unknown named parameter $time_limit in ...:...
Overwriting Parameters
It is not allowed to overwrite a parameter that's set before:
function test($a, $b, ...$args) {}
test(a: 28, b: 15, a: 45);
Fatal error: Uncaught Error: Named parameter $a overwrites previous argument in ...:...
Variable Named Parameters
The parameter name cannot be a variable, and will result in a syntax error.
str_replace($key: "Inevitable", $value: "Iron Man", subject: "I am Inevitable");
Parse error: syntax error, unexpected token ":", expecting ")" in ... on line ...
For call-sites with variable parameter names, it is possible to use call_user_func_array()
function for this, but be cautious when accepting a user-input with this function.
$key = 'search';
$value = 'replace';
$params = [
$key => 'Inevitable',
$value => 'Iron Man',
'subject' => 'I am Inevitable',
];
call_user_func_array('str_replace', $params);
See call_user_func_array
Considerations for caveats when using this pattern.
Mixing Named and Positional Parameters
It is possible to use named parameters and positional parameters in the same call. Named parameters must be used after positional parameters.
setcookie("Awesome", "Yes",
path: "/",
domain: "example.com",
secure: true,
httponly: true
);
In the snippet above, the first two parameters are positional parameters, as they are passed in the same order they are declared. The last four parameters use named parameters, and the call passes all required parameters, making it a valid call.
However, it is not allowed to use named parameters before positional parameters, if they are used at all:
setcookie(
name: "Awesome",
"Yes",
path: "/",
domain: "example.com",
secure: true,
httponly: true
);
Fatal error: Cannot use positional argument after named argument in ... on line ...
The snippet above has a named parameter for the $name
parameter, but uses positional parameter for the second parameter. This is not allowed because the parser cannot correctly infer the position of the parameter once named parameters are used in a call-site.
call_user_func_array
Considerations
The call_user_func_array()
function will use they array keys as parameter names, and this can be a breaking-change in certain situations, where the parameter array is an associative array with non-numeric keys.
$params = [
'replace' => 'Iron Man',
'search' => 'Inevitable',
'subject' => 'I am Inevitable',
];
echo call_user_func_array('str_replace', $params);
In the snippet above, only PHP 8.0 and later will consider named parameters, and in prior versions, the arguments will be passed as positional parameters.
The snippet below will return different values in PHP < 8.0 and PHP >= 8.0:
$params = [
'replace' => 'Iron Man',
'search' => 'Inevitable',
'subject' => 'I am Inevitable',
];
echo call_user_func_array('str_replace', $params);
PHP < 8.0
I am Inevitable
PHP >= 8.0
I am Iron Man
Due to PHP's back-porting policy, it is unlikely that PHP 7.4 will emit any warnings of potentially different interpretations in PHP 8.0.
In practice, this means all call_user_func_array()
function calls must be aware that PHP 8.0 will interpret associative arrays and numeric arrays different.
As a precautionary measure, call_user_func_array()
calls can use array_values
if the parameters array might contain non-numeric keys.
$params = [
'replace' => 'Iron Man',
'search' => 'Inevitable',
'subject' => 'I am Inevitable',
];
echo call_user_func_array('str_replace', array_values($params));
With array_values
call, PHP will always use the positional calling pattern, ensuring the outcome will be the same in PHP 8.0 and later.
call_user_func_array()
behaves differently if the parameters array is an associative array. There is a non-zero backwards-compatible impact due to this, and is the only potential backwards-compatibility issue related to named parameters.
Argument Unpacking
PHP supports argument unpacking feature since PHP 5.6, but it did not allow arrays with non-numeric keys, to ensure there will be no backward compatibility issues when named parameters are implemented.
With PHP 8.0 named parameters, it is now possible to use argument unpacking operator with names parameters, although directly using named parameters might be more readable.
str_replace(...[
'replace' => 'Iron Man',
'search' => 'Inevitable',
'subject' => 'I am Inevitable',
]);
Named Parameter with Inheritance Chains
PHP's Named Parameters feature allows renaming the parameter names.
class Foo {
public function doSomething(string $param) {}
}
class SubFoo extends Foo{
public function doSomething(string $myParam) {}
}
When using named parameter, the parameter of the called class must be used, and parameter name of the parent class/classes are not considered.
For example, in the simple inheritance chain above, SubFoo::doSomething
methods renames the param
parameter to myParam
. This is allowed, but when they are used, the renamed parameter name must be used.
$foo = new SubFoo();
$foo->doSomething(myParam: "test");
Calling methods with the previous parameter names will cause an error, as if an unknown parameter is passed.
$foo = new SubFoo();
$foo->doSomething(param: "test");
Fatal error: Uncaught Error: Unknown named parameter $param in ...:...
Behavior of func_get_arg(s)
and func_num_args()
With positional parameters, it is possible to pass more parameters that what the function declaration accepts. This pattern is now allowed with named parameters, due to the unknown parameter rule. To collect excessive parameter values without unknown parameter restrictions, use [variadic parameters]{#named-params-variadic-params}
function test(){}
test(a: 28, b: 15);
Fatal error: Uncaught Error: Unknown named parameter $a in ...:...
func_get_arg()
and func_get_args()
still work with named parameters, but they behave as if positional parameters were used at call-site.
func_get_arg()
returns the values in the order they are declared in the function; not the order they were used in a named parameter call-site.
function test($a, $b){
var_dump(func_get_arg(0));
}
test(b: 15, a: 28); // 28
func_get_args()
returns a numeric array in the order parameters are declared, not called, which is the exact same behavior in prior PHP versions.
function test($a, $b){
var_dump(func_get_args());
}
test(b: 15, a: 28);
array(2) {
[0]=> int(28)
[1]=> int(15)
}
It is not possible to get a parameter by the named parameter from call-site: e.g. func_get_arg('c')
.
Named Parameters with Variadic Parameters
A variadic parameter can collect parameters not assigned to a previous parameter (if any), and their names will be preserved.
function test($a, $b, ...$args) {
var_dump($args);
}
test(28, 15, june: 6, september: 15, january: "01");
array(3) {
["june"]=> int(6)
["september"]=> int(15)
["january"]=> string(2) "01"
}
This behavior aligns with argument unpacking, that it is possible to pass a variadic arguments list to a callback.
function run_hook(callable $callable, ...$args): mixed {
return $callable($args);
}
function run_hook(callable $callable, ...$args): mixed {
return $callable(...$args);
}
run_hook('str_replace',
search: 'Inevitable',
replace: 'Iron Man',
subject: 'I am Inevitable'
); // "I am Iron Man"
Named Parameters in Public APIs
With named parameters, the name of the function/method parameters become part of the public API, and changing the parameters of a function will be a semantic versioning breaking-change. This is an undesired effect of named parameters feature.
Opting-out of Named Parameters
Except for a variadic parameter containing named parameter, both positional and named parameters will behave similarly within the called function.
Some PHP projects have started to use @no-named-parameters
DocBlock attribute to indicate that the function does not prefer to be called with named parameters, and does not provide backwards-compatibility for parameter named.
Backwards Compatibility Impact
The only backwards-compatibility impact is with call_user_func_array()
, that it considers an associative array as a named parameter call. This can be mitigated by "normalizing" the parameters array with array_values
call:
$params = [
'replace' => 'Iron Man',
'search' => 'Inevitable',
'subject' => 'I am Inevitable',
];
echo call_user_func_array('str_replace', array_values($params));
This ensures the code behaves exactly the same in all PHP versions.
Attributes
One of the biggest new changes in PHP 8 is the Attributes support. Attributes help to add meta-data to PHP functions, parameters, classes, class methods, constants, properties, closures, and even anonymous classes. This meta-data can be fetched programmatically, and provides a pragmatic approach to resolve the attributes in elsewhere in the code.
An in-depth post about the history, different approaches, and practical use cases of Attributes are in the post Attributes in PHP 8.
Syntax and Usage
Attributes are declared with #[
and ]
braces.
#[ExampleAttribute('foo', 'bar')]
function example() {}
Attributes syntax was initially implemented as
<<Attribute>>
, followed by@@Attribute
, and finally as#[Attribute]
.
In all PHP versions, #
starts a comment for the rest of the line. When an attribute is used in its own line, it will not cause a syntax error in PHP versions prior to 8.0. This makes code that uses PHP 8.0 attributes somewhat backwards compatible, as long as each attribute is attributed in its own line, which makes PHP ignore those attributes.
Attributes may resolve to class names
Although not required, PHP 8 provides functionality to resolve the attribute names to class names. You can use use
statements to clean-up the code. Standard rules of class name resolving will be followed.
It is optional to match the Attribute name to a class name.
If an attribute does not map to a class, the Reflection API does not allow to fetch an instance of the instantiated attribute.
Attributes can have parameters
Each attribute can have zero or more parameters. They will be passed to the Attribute class constructor if attempted to get an instantiated object of the attribute.
Parameter can be simple scalar types, arrays, or even simple expressions such as mathematical expressions, PHP constants, class constants (including magic constants). Any expression that can be used as a class constant can be used as Attribute parameter.
More than one attribute allowed
Each item that receives Attributes can have zero or many attributes, each in its own #[
]
brackets, or inside the same brackets separated by a comma.
Each Attribute can be separated by a white-space (either a new line or a space(s)).
#[Attr]
#[FooAttr]
function foo(){}
#[Attr, FooAttr]
function bar(){}
By default, the same attribute is not allowed to be used more than once. However, it is possible to declare an attribute that explicitly allows repeated use.
Before and After DocBlock comments
Attributes can appear before and after DocBlock comments. There is no standard recommendation for the code style, but this surely will be ironed out in a future PSR code-style recommendation.
Declaring Attributes
In order to use attributes, the attribute itself may be declared as a class. This is validated only when the attribute is fetched, and not immediately when the code is parsed.
A PHP attribute is a standard PHP class, declared with #[Attribute]
attribute.
#[Attribute]
class FooAttribute{
}
By default, a declared attribute can be used on any item that accepts attributes. This includes classes, class methods, closures, functions, parameters, and class properties.
When declaring the attribute, it is possible to declare the targets the attribute must be used.
#[Attribute(Attribute::TARGET_CLASS)]
class Foo {}
When the attribute is attributed with the targets it supports, PHP does not allow the attribute to be used on any other targets. It accepts a bit-mask to allow the attribute in one or more targets.
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Foo {}
It allows the following targets:
Attribute::TARGET_ALL
Attribute::TARGET_FUNCTION
Attribute::TARGET_METHOD
Attribute::TARGET_PROPERTY
Attribute::TARGET_CLASS_CONSTANT
Attribute::TARGET_PARAMETER
Attribute::TARGET_METHOD
TARGET_ALL
is the OR
of all other targets.
Attribute
class is declared final
The Attribute
class is declared final
, which prevents it from being extended.
Repeatable Attributes
By default, it is not allowed to use the same attribute on the same target more than once. The attribute must explicitly allow it:
#[Attribute(Attribute::IS_REPEATABLE)]
class MyRepeatableAttribute{}
Attribute Syntax Example
#[FooAttribute]
function foo_func(#[FooParamAttrib('Foo1')] $foo) {}
#[FooAttribute('hello')]
#[BarClassAttrib(42)]
class Foo {
#[ConstAttr]
#[FooAttribute(null)]
private const FOO_CONST = 28;
private const BAR_CONST = 28;
#[PropAttr(Foo::BAR_CONST, 'string')]
private string $foo;
#[SomeoneElse\FooMethodAttrib]
public function getFoo(#[FooClassAttrib(28)] $a): string{}
}
// Declare Attributes
/*
* Attributes are declared with `#[Attribute]`.
*/
#[Attribute]
class FooAttribute {
public function __construct(?string $param1 = null) {}
}
#[Attribute]
class ClassAttrib {
public function __construct(int $index) {}
}
Looking for fine-grained examples ?
Attributes in PHP 8 contains several more examples with edge-cases demonstrated.
Fetching Attributes
Attributes are retrieved using the Reflection API. When PHP engine parses code that contains Attributes, they are stored in internal structures for future use. Opcache support included. It does not execute any code or call the constructors of the attributes unless an instance of the Attribute is requested (see examples below).
Using the Reflection API, the Attributes can be retrieved either as strings that contain the Attribute name (with class names resolved), and its optional arguments.
Reflection API can also instantiate an instance of the Attribute class, with class names resolved, auto-loaded, and the optional parameters passed to the class constructor. Failure to instantiate the class will throw \Error
exceptions that can be caught at the caller level.
New Reflection*::getAttributes()
method
$reflector = new \ReflectionClass(Foo::class);
$reflector->getAttributes();
All Reflection*
classes get a new method getAttributes
method, that returns an array of ReflectionAttribute
objects. A synopsis of this new method would be similar to the following:
/**
* @param string $name Name of the class to filter the return list
* @param int $flags Flags to pass for the filtering process.
* @return array ReflectionAttribute[]
*/
public function getAttributes(?string $name = null, int $flags = 0): array {}
ReflectionAttribute
class synopsis
final class ReflectionAttribute {
/**
* @return string The name of the attribute, with class names resolved.
*/
public function getName(): string {}
/**
* @return array Arguments passed to the attribute when it is declared.
*/
public function getArguments(): array {}
/**
* @return object An instantiated class object of the name, with arguments passed to the constructor.
*/
public function newInstance(): object {}
}
Attribute filtering
Reflection*::getAttributes()
optionally accepts a string of class name that can be used to filter the return array of attributes by a certain Attribute name.
$attrs = $reflector->getAttributes(FooAttribute::class);
$attrs
array would now be only ReflectionAttribute
objects or FooAttribute
Attribute name.
A second optional parameter accepts an integer to further fine tune the return array.
$attrs = $reflector->getAttributes(BaseAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);
At the moment, only \ReflectionAttribute::IS_INSTANCEOF
is available.
If \ReflectionAttribute::IS_INSTANCEOF
is passed, the return array will contain Attribute with same class name or classes that extends
or implements
the provided name (i.e all classes that fulfull instanceOf $name
).
Retrieving Attribute Objects
ReflectionAttribute::newInstance
method returns an instance of the Attribute class, with any parameters passed to the Attribute object class constructor.
A complete example
#[ExampleAttribute('Hello world', 42)]
class Foo {}
#[Attribute]
class ExampleAttribute {
private string $message;
private int $answer;
public function __construct(string $message, int $answer) {
$this->message = $message;
$this->answer = $answer;
}
}
$reflector = new \ReflectionClass(Foo::class);
$attrs = $reflector->getAttributes();
foreach ($attrs as $attriubute) {
$attriubute->getName(); // "My\Attributes\ExampleAttribute"
$attriubute->getArguments(); // ["Hello world", 42]
$attriubute->newInstance();
// object(ExampleAttribute)#1 (2) {
// ["message":"Foo":private]=> string(11) "Hello World"
// ["answer":"Foo":private]=> int(42)
// }
}
Read in-depth: This is a short summary of the new Attributes feature in PHP 8. For a detailed guide, see Attributes in PHP 8
Backwards Compatibility Impact
The final syntax of Attributes feature is not backwards-compatible. However, because the #
character is a valid symbol to start a comment, an attribute declared on its own line can make the code parse and run on earlier PHP versions.
#[ExampleAttribute('Hello world', 42)]
class Foo {}
If an attribute is used within the same line, the parser of prior PHP versions will not work:
function foo(#[TestAttr] $bar){
}
Parse error: syntax error, unexpected '}', expecting variable (T_VARIABLE) in ... on line ...
Class constructor property promotion
Constructor Property Promotion is a new syntax in PHP 8 that allows class property declaration and constructor assignment right from the constructor.
A typical class that declares a property, and then assigns a value to it in the class constructor is quite verbose.
class User {
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
}
With the Constructor Property Promotion syntax, the class declaration above can be minimized to avoid boilerplate:
class User {
- private string $name;
- public function __construct(string $name) {
+ public function __construct(private string $name) {
- $this->name = $name
}
}
This results in a much simplified and minimal syntax that is functionally identical to the verbose class declaration:
class User {
public function __construct(private string $name) {}
}
What it does
Constructor Property Promotion is a shorthand syntax to declare and assign class properties from the constructor. This avoids having to type the class property name from four times to just once, and property type from twice to just once.
Your constructor can still run additional code within the constructor. Properties will be assigned before the rest of the constructor code is executed.
class User {
public function __construct(private string $name) {
echo $this->name;
}
}
new User('Ayesh');
// echoes "Ayesh"
If you change the constructor argument, you need to re-assign it to the class property:
new User('Ayesh');
class User {
public function __construct(public string $name) {
echo $this->name; // "Ayesh"
$name = 'foo' . $name;
echo $this->name; // "Ayesh"
$this->name = $name;
echo $this->name; // "fooAyesh"
}
}
AST and Reflection
Constructor Property Promotion is not internally handled as an Abstract Syntax Tree transformation. This means if you inspect the AST (with an extension such as php-ast), you will still see the code as written with property visibility modifiers right in the constructor.
Reflection API, it will seamlessly return information about both standard and constructor-promoted properties all the same.
In Reflection API, ReflectionProperty
and ReflectionParameter
classes will have a new method isPromoted
to determine if the parameter/property was declared in a constructor.
Syntax
Mixing constructor properties with standard properties
A class is allowed to have constructor-promoted properties and standard properties:
class User {
private int $uid;
public function __construct(public string $name, int $uid) {
$this->uid = $uid;
}
}
However, note that the same property must not be declared as a standard property and a constructor-promoted property.
Property Type is not required
It is possible to use Typed Properties and un-typed properties, although it's often a good idea to always strict type class properties.
class User {
public function __construct(private string $name, private $uid) {}
}
In the example above $uid
will be a class property, but without a type declared to it.
Nullable types are supported, but not implicit syntax
Nullable properties can be promoted at constructor, however, the PHP 7.0-style nullable types are not supported.
class User {
public function __construct(
public ?string $name,
public ?string $fullname = null,
) {}
}
While the syntax above is allowed, implicit declaration of types (without the ?
prefix, but = null
default value) is not allowed:
class User {
public function __construct(
public string $name = null,
public string $fullname = null,
) {}
}
Fatal error: Cannot use null as default value for parameter $name of type string in ... on line ...
Both property promotions above are not allowed because they are implicitly declaring nullable types, but without the ?string
syntax. Although it works, this practice is frowned upon.
No duplicate properties
It is not allowed to duplicate a property with explicit declaration and constructor property promotion.
The following code is not allowed:
class User {
public string $name;
public function __construct(public string $name) {}
}
Fatal error: Cannot redeclare User::$name in ... on line ...
Not supported in interfaces and abstract classes, allowed in traits
Constructor Properties cannot be used in interfaces because they simply don't allow properties in the first place.
Not allowed:
interface User {
public function __construct(public string $name);
}
Abstract classes can have constructor properties promoted, but not if the constructor itself is marked abstract
.
abstract class User {
abstract public function __construct(public string $name) {}
}
Fatal error: Cannot declare promoted property outside a constructor in ... on line ...
Traits can contain both properties and methods, including constructors. This means it is possible for a trait for make use of Constructor Property Promotion too.
trait UserTrait {
public function __construct(public string $name) {}
}
var
is not allowed
The legacy syntax of declaring a property with var
keyword is not allowed. All constructor-promoted properties must be declared with public
, protected
, or private
visibility modifiers.
The following code is not valid:
class User {
public function __construct(var $username) {}
}
Parse error: syntax error, unexpected 'var' (T_VAR), expecting variable (T_VARIABLE) in ... on line ...
callable
type is not allowed
callable
is a valid type in PHP, but it is not allowed as a property type. Thus, it is not allowed to declare a constructor property with type callable
either:
class User {
public function __construct(public callable $logoutFn) {}
}
Fatal error: Property User::$logoutFn cannot have type callable in ... on line ...
Only constructor supports property promotion
It probably goes without saying, but only the __construct
method can have properties promoted. Other methods cannot:
class User {
public function setUser(public string $name) {}
}
Fatal error: Cannot declare promoted property outside a constructor in ... on line ...
Backwards compatibility impact
Constructor Property Promotion is a syntax change in PHP 8, and so it's not possible to backport this feature to older PHP versions. If you use Constructor Property Promotion, that code will not be compatible with PHP 7 and older versions.
Attempting to run constructor-promoted classes will throw a parse error:
Parse error: syntax error, unexpected 'public' (T_PUBLIC), expecting variable (T_VARIABLE) in ... on line ...
Derick Rethans interviewed Nikita Popov (author of the RFC) in PHP Internals News podcast, which you can list here.
JIT
PHP 8.0 brings support for Just-In-Time Compilation (JIT).
Pure interpreted programming languages has no compilation step, and directly executes the code in a virtual machine. Most of the interpreted languages including PHP, in fact, has a light-weight compilation step to improve its performance.
Programming languages with Ahead-Of-Time (AOT) compilation, on other hand requires the code to be compiled first before it runs.
Just-In-Time compilation is a hybrid model of interpreter and Ahead-of-Time compilation, that some or all of the code is compiled, often at run-time, without requiring the developer to manually compile it.
PHP was historically an interpreted language, that all of the code was interpreted by a virtual machine (Zend VM). This was changed with the introduction of Opcache and Opcodes, which were generated from the PHP code, and can be cached in memory. PHP 7.0 added the concept of AST (Abstract Syntax Tree), that further separated the parser from the compiler.
PHP's JIT internally uses DynASM from LuaJIT, and as implemented as part of the Opcache.
JIT In Depth
A detailed guide on optimal JIT configuration, benchmarks, and how JIT works in detail
Opcache can inspect the used code, commonly called hot code, and store the compiled versions of them within the shared Opcache memory. When the code should be compiled, and what code should be compiled is configurable.
Platform Support
JIT is currently enabled on Linux and Windows systems running on x86 and x64 processor instruction sets. Apple M1, and ARM CPUs are currently not supported.
DynASM, which is the underlying assembler used in PHP JIT, supports ARM instructions as well, but it is not clear if PHP JIT can be run on ARM processors yet.
JIT also makes use of AVX if it is supported on the CPU. Most consumer and server grade processors from 2011 and later support AVX instructions. cat /proc/cpuinfo | grep avx
run on most POSIX systems should reveal if the processor supports it.
Configuring JIT
In PHP 8.0, JIT is enabled by default, but turned off. Enabling JIT is easy, and only requires minimal INI configuration similar to this:
opcache.enable=1
opcache.enable_cli=1
opcache.jit_buffer_size=256M
Enable Opcache (opcache.enable
)
JIT is implemented as part of Opcache, and requires the Opcache extension to be enabled.
opcache.enable=1
opcache.enable_cli=1
opcache.enable=1
directive does not enable Opcache for CLI, and requiresopcache.enable_cli=1
. It is possible to turn off Opcache (and this JIT) for other SAPIs (such as FPM), and enable Opcache only for CLI withopcache.enable_cli
directive.
Set Buffer Size (opcache.jit_buffer_size
)
The effective toggle for JIT is opcache.jit_buffer_size
. It accepts how much memory JIT is allowed to use for its buffer.
The default value 0
, which effectively disables JIT.
To enable and set a buffer size, set a positive value in bytes, or with standard PHP data size suffixes (M
, G
, etc.)
opcache.jit_buffer_size=256M
Additional JIT Configuration Options
The following configuration options are available, but they are set to appropriate defaults.
Set JIT flags (opcache.jit
)
opcode.jit
is a somewhat complicated configuration value. It accepts disable
, on
, off
, trace
, function
, and a 4-digit value (not a bitmask) of 4 different flags in the order.
disable
: Completely disables JIT feature at start-up time, and cannot be enabled run-time.off
: Disabled, but it's possible to enable JIT at run-time.on
: Enablestracing
mode.tracing
: An alias to the granular configuration1254
.function
: An alias to the granular configuration1205
.
Granular Configuration
In addition to the tracing
and function
aliases, the opcache.jit
directive accepts a 4-digit configuration value as well. it can further configure the JIT behavior.
The 4-digit configuration value is in the form of CRTO
, where each position allows a single digit value for the flag designated by the letter.
For the full configuration options, see JIT Flags.
The default value is tracing
, which compiles hot code (call-graph) while the code is being run (tracing), and enables use of CPU registers and AVX extension.
JIT In Depth
A detailed guide on optimal JIT configuration, benchmarks, and how JIT works in detail
Debugging JIT (opcache.jit_debug
)
PHP JIT provides a way to emit JIT debug information by setting an INI configuration. When set, it outputs the assembly code for further inspection.
opcache.jit_debug=1
The opcache.jit_debug
directive accepts a bit-mask value to toggle certain features. It currently accepts a value in the range of 1 (0b1
) to 20 bits binary. A value of 1048576
represents the maximum level of debug output.
php -d opcache.jit_debug=1 test.php
TRACE-1$/test.php$7: ; (unknown)
sub $0x10, %rsp
mov $EG(jit_trace_num), %rax
mov $0x1, (%rax)
.L1:
cmp $0x4, 0x58(%r14)
jnz jit$$trace_exit_0
cmp $0x4e20, 0x50(%r14)
...
More Configuration Options
JIT supports several other configuration options to tune how many function calls makes it a "hot" function, which JIT then compiles, and a threshold to consider which functions to JIT, based on the percentage of overall function calls.
For a full list, see Opcache Configuration.
JIT Flags
The opcache.jit
directive accepts a 4-digit value to control the JIT behavior, in the form of CRTO
, and accepts following values for C
, R
, T
, and O
positions.
C
: CPU-specific Optimization Flags
0
: Disable CPU-specific optimization.1
: Enable use of AVX, if the CPU supports it.
R
: Register Allocation
0
: Don't perform register allocation.1
: Perform block-local register allocation.2
: Perform global register allocation.
T
: Trigger
0
: Compile all functions on script load.1
: Compile all functions on first execution.2
: Profile first request and compile the hottest functions afterwards.3
: Profile on the fly and compile hot functions.4
: Currently unused.5
: Use tracing JIT. Profile on the fly and compile traces for hot code segments.
O
: Optimization Level
0
: No JIT.1
: Minimal JIT (call standard VM handlers).2
: Inline VM handlers.3
: Use type inference.4
: Use call graph.5
: Optimize whole script.
The option
4
under Triggers (T=4
) did not make it to the final version of JIT implementation. It was trigger JIT on functions declared with@jit
DocBlock comment attribute. This is now unused.
Backwards Compatibility Impact
None. JIT is a new feature added in PHP 8.0, and should not cause any issues. PHP does not raise any warnings or errors when it encounters unknown INI directives, which means setting JIT INI directives would not cause any issues.
New `%h` and `%H` `printf` specifiers
PHP 8.0 adds new %h
and %H
format specifiers for printf
class of functions. They work similar to the existing %g
and %G
modifiers, but are locale-independent.
printf('%h', 3.141592653589793238); // 3.14159
printf('%H', 3.141592653589793238); // 3.14159
Prior to PHP 8.0, float
to string
conversions were locale-aware. This is changed in PHP 8.0. The existing %g
and %G
specifiers remain locale-aware, and the new %h
and %H
are their locale-independent counterparts.
setlocale(LC_ALL, "fr_FR");
// Locale-aware, with fr_FR comma decimal separator
printf('%g', 3.141592653589793238); // 3,14159
printf('%G', 3.141592653589793238); // 3,14159
// Locale-independent
printf('%h', 3.141592653589793238); // 3.14159
printf('%H', 3.141592653589793238); // 3.14159
Backwards Compatibility Impact
If %h
and %H
specifiers are used in PHP versions prior to 8.0, they will be processed to empty strings without raising any errors.
Related Changes
New `*` precision and width modifiers in `printf`
printf
and its related functions (such as sprintf
) in PHP 8.0 support specifying the width/precision modifiers as a function parameter rather than hard-coding it to the format string itself.
%f
, the float formatter supports specifying the precision with%.3f
, where thefloat
is represented with a precision of3
. Similarly,%.5f
is for precision 5, etc.%s
, the string formatter supports specifying the length of the string.%.5
would limit the string to 5 bytes,%.10s
is for the first 10 bytes, etc.
In PHP 8, the width and precision modifiers can be set as a parameter to the printf
and other related functions. The width/precision modifier is indicated with *
, and should follow the same sequential order the printf
functions otherwise accept.
printf('%.*f', 3, 1.61803); // "1.618"
printf('%.3f', 1.61803); // "1.618"
In the snippet above, the first printf
call uses %.*f
format, and the precision is set as the second parameter. The result is identical to the second printf
call, where the precision is already specified in the format itself.
Positional parameters support the *
modifiers as well:
printf('%.*2$f', 1.61803, 3); // "1.618"
// ^^ ^
printf('%3$.*4$f', 42, 42, 1.61803, 3); // "1.618"
// ^^ ^
Precision must me an integer
Note that the precision must be an integer, and there is no type coercion even with strict typing is off. Attempting to set a precision with any value other than an integer will throw a ValueError
exception.
printf('%.*f', (string) 3, 1.618);
// Fatal error: Uncaught ValueError: Precision must be an integer in ...:...
Precision -1
is allowed in %g
/%G
and [%h
/%H
]
%g
/%G
and %h
/%H
specifiers allow -1
as the precision. For width and precision modifiers, only 0
and positive integers are allowed otherwise.
Backwards Compatibility Impact
Precision modifier and width modifiers are not understood in PHP versions prior to 8.0. They will not raise any warnings or exceptions.
Both width (%*
) and precision (%.*
) will be evaluated to empty strings.
PHP < 8.0 | PHP >= 8.0 | |
---|---|---|
printf('%.*f', 3, 1.61803) |
f |
1.618 |
printf('%.3f', 1.61803) |
1.618 |
1.618 |
printf('%*.*f', 7, 3, 1.61803) |
.*f |
1.618 |
printf('%7.3f', 1.61803) |
1.618 |
1.618 |
printf('%*d', 5, 17) |
d |
17 |
printf('%5d', 17) |
17 |
17 |
The *
modifiers are merely syntactic changes, and hardcoding the precision/width values in the format string itself ensures backwards compatibility with PHP versions prior to 8.0.
Related Changes
New `Stringable` interface
PHP 8.0 adds a new interface named Stringable
, that indicates any classes implementing the Stringable
interface implement a __toString(): string
magic method.
To provide forward compatibility, PHP engine automatically adds the Stringable
interface to all classes that implement a __toString()
method.
interface Stringable {
public function __toString(): string;
}
The most common use case would be a strict type inside a function/method before it attempts to use a string function.
class Foo {
public function __toString(): string {
return 'FooBar';
}
}
function safePrint(string|Stringable $input): void {
echo htmlspecialchars((string) $input);
}
The safePrint
function can use Union Types to indicate and enforce that it accepts scalar string
types, or an object of a class that implements Stringable
interface.
Optional to declare implements \Stringable
If a class implements a __toString
method, PHP automatically considers that it implements the Stringable
interface. It is not required, but possible to explicitly declare it.
class Foo implements \Stringable{
public function __toString(): string {
return 'FooBar';
}
}
Checking string capabilities
With both scalar string
type and the new Stringable
interface, it is now possible to safely use string functions in an application that types are enforced.
$foo instanceof \Stringable
instanceof
checks will return true
for classes that implement __toString
method with or without explicit implements \Stringable
declaration.
class Foo {
public function __toString(): string {
return 'FooBar';
}
}
$foo = new Foo();
var_dump($foo instanceof Stringable); // true
Note that instanceof
currently requires an object, and does not accept scalar variables yet.
class_implements($foo)
class_implements
function will correctly return that a given object from a class that has a __toString()
method with or without an explicit implements \Stringable
declaration will return Stringable
as one of the interfaces the class implements.
class Foo {
public function __toString(): string {
return 'FooBar';
}
}
var_dump(class_implements(new Foo()));
// array("Stringable" => "Stringable")
is_string($foo)
The is_string
function checks the type of the variable and return true
only if the type of the parameter provided is string
. Because objects from classes that implement __toString
are objects, is_string()
function returns false
on objects even if they comply with Stringable
interface with or without explicit declaration.
class Foo {
public function __toString(): string {
return 'FooBar';
}
}
var_dump(is_string(new Foo())); // false
strict_types = 1
behavior
Stringable
objects will be evaluated to strings with __toString
when strict types are not enforced. If strict typing is enforced (with declare(strict_types=1)
at the top of the file), an explicit string cast is required.
declare(strict_types=1);
class Foo {
public function __toString(): string {
return 'FooBar';
}
}
function safePrint(string|Stringable $input): void {
echo htmlspecialchars($input);
//^^^ No string cast
}
safePrint(new Foo());
// Fatal error: Uncaught TypeError: htmlspecialchars(): Argument #1 ($string) must be of type string, Foo given in ...:...
In the snippet above, strict types are enforced. htmlspecialchars
function only accepts string
parameters. In case an object of Stringable
interface is passed, PHP will refuse to accept it even though the object implements Stringable
interface.
When strict types are enabled, make sure to cast
string|Stringable
values to strings with(string) $input
.
Backwards Compatibility Impact
It is trivial to bring the new Stringable
interface to PHP versions prior to 8.0. However, note that unless the class explicitly declares implements \Stringable
, PHP versions prior to 8.0 will not provide the string-capability checks as mentioned above.
interface Stringable {
public function __toString(): string;
}
class MyStringCapableClass implements Stringable {
public function __toString(): string {
return 'Hello World';
}
}
For the compatibility for prior PHP versions, the Stringable
class must be added, and all classes providing string capabilities must explicitly declare that they implements \Stringable
.
Related Changes
- Class magic method signatures are strictly enforced
- Union Types
- Locale-independent
flaot
tostring
casting - New
mixed
pseudo type
Built-in web server supports dynamic port selection
The built-in server in PHP 8.0 supports dynamic port selection.
php -S localhost:0
With the port 0
specified, an unused port is picked by the operating system, and the built-in web server will be bound to that port number.
> php -S localhost:0
[Thu Oct 1 02:35:59 2020] PHP 8.0.0 Development Server (http://localhost:11583) started
For every run of this command, a random unused port will be selected by the operating system. Note that this behavior can be different from one operating system to another.
Obtain the allocated port number
To obtain the selected port number within the application being run, use $_SERVER['SERVER_PORT']
super global variable.
Port 0
Port 0 is an IANA reserved port number. While not a standard, many operating systems support binding a service to port 0, which requests the next available port from the system. This functionality is available in most operating systems TCP/IP stacks, including Windows and Linux.
PHP historically disallowed binding to port 0, but from PHP 8.0 and forward, it is now possible to specify port 0 for the built-in server, and it will use an available unoccupied non-system (> 1024) port number.
In other languages
Built-in servers such as Node JS server and Python server already support this pattern to pick a port number dynamically.
- Python:
sock.bind(('',0))
- Node JS:
server.listen(0, 127.0.0.1)
- Rust:
TcpListener::bind("127.0.0.1:0").unwrap()
Backwards Compatibility Impact
"Binding" to port 0 is not allowed in PHP versions prior to 8.0, and result in an error:
> php -S localhost:0
Invalid address: localhost:0
Sockets extension can bind to port 0 in all PHP versions, and it might help to find a free port, after which it can be used to start a PHP server.
$socket = \socket_create_listen(0);
\socket_getsockname($socket, $addr, $port);
\socket_close($socket);
echo $port;
Note that in PHP 8, socket
extension uses \Socket
class objects instead of resource
data types for its primary data type.
Stack trace as string - Parameter max length is configurable
Exceptions thrown in PHP can optionally print the exception information on screen, or return a string with the exception, including the stack trace.
function test() {
try {
throw new \Exception();
}
catch (Exception $ex) {
echo $ex;
}
}
test('1234567890123456789);
Exception in ...:...
Stack trace:
#0 ...(...): test('123456789012345...')
By default, the length of the parameters in the stack trace is truncated to 15 bytes, followed by ...
. Notice how the line is truncated to 15 bytes, followed by an ellipsis.
test('123456789012345...')
In PHP 7.4, a new INI setting named zend.exception_ignore_args
was added, which can completely hide the parameter information from stack trace.
zend.exception_ignore_args=false
An INI directive like above will completely hide the parameters in stack trace.
In PHP 8.0, there is a new INI directive to limit the length of the parameters shown in stack trace. Note that the zend.exception_ignore_args
directive can still completely hide the parameter information from stack traces.
zend.exception_string_param_max_len=42
This somewhat lengthy but unambiguous directive can expand the stack traces to show more information in the stack trace. They can help ease debugging because a maximum length of 15 bytes is often not enough for URLs (https://www.
for example is already 12 bytes, leaving only three bytes for the remaining of it), database queries, etc.
To prevent a misconfigured configuration from writing or outputting too much data due this change, there is a hard limit of 1000000 bytes to this directive.
Note that this change only affects when a Throwable
(see hierarchy of PHP exceptions) is coerced to a string
, explicitly retrieved as a string, or when an uncaught exception is printed on screen.
- When a
\Throwable
is coerced to string, such as(string) $exception
orecho $exception
. - When a
\Throwable
is retrieved as string, with$exception->getTraceAsString()
, i.e.Throwable::getTraceAsString()
. - Default errors printed on screen when an exception is uncaught.
This change does not affect debug_print_backtrace
function or Throwable::getTrace
method, as they both return the full stack trace.
Backwards Compatibility Impact
The default configuration value of zend.exception_string_param_max_len
remains 15
bytes. This means unless this value is not updated, the stack traces would be the same.
Related Changes
- Default error reporting is set to
E_ALL
- Assertions throw exceptions by default
- Expressions can now throw Exceptions
catch
exceptions only by type
WeakMaps
Weak Maps is a new feature added in PHP 8.0.
A WeakMap allows to store and associate arbitrary values for object keys, but without preventing the garbage collector from clearing the object if it falls out of scope in everywhere else.
Weak Maps in PHP 8.0 is different from the
WeakMap
class from theweakref
PECL extension. See PHP WeakMap Practical Use Cases: PECL extension for more information.
A WeakMap
is similar to SplObjectStorage
, as in both WeakMap
and splObjectStorage
use objects
s as the key, and allows storage of arbitrary values. However, a WeakMap
does not prevent the object from being garbage collected.
$map = new splObjectStorage();
$object = new stdClass();
$map[$object] = 'Foo';
var_dump(count($map)); // int(1)
unset($object);
var_dump(count($map)); // int(1)
In the snippet above, splObjectStorage
is used to store additional data about objects. Even though there is an unset($object)
call, PHP's garbage collector does not clear it from memory because of the reference within the $map
splObjectStorage
.
With WeakMap
, the references are weak, and the reference inside the WeakMap
does not prevent the garbage collector from clearing the object if there are no other references to the object.
- $map = new splObjectStorage();
+ $map = new WeakMap();
$object = new stdClass();
$map[$object] = 'Foo';
var_dump(count($map)); // int(1)
unset($object);
var_dump(count($map)); // int(0)
A more simplified example would be:
$map = new splObjectStorage();
$map[new stdClass()] = 'Foo';
count($map); // 1
$map = new WeakMap();
$map[new stdClass()] = 'Foo';
count($map); // 0
new stdClass
is used as the key, and it immediately falls out of scope. WeakMap
allows the object to be cleared, while splObjectStorage
does not.
Weak Maps can help associate transient data for objects without changing the state of the object itself. If the object falls out of scope, so does the associated data.
Complete Example
class CommentRepository {
private WeakMap $comments_cache;
public function getCommentsByPost(Post $post): ?array {
if (!isset($this->comments_cache[$post])) {
$this->comments_cache[$post] = $this->loadComments($post);
}
return $this->comments_cache[$post]
}
}
In the example above, the CommentRepository
class uses WeakMap
class to temporarily store comments in memory, stored as typed property $comments_cache
. When the getCommentsByPost
method is called with a Post
object, the $comments_cache
is checked and returned first, before loading them from the database with private loadComments
method.
If the CommentRepository
class were to store the comments in array (indexed by the post ID), or in SplObjectStorage
, this cache will not be cleared even if the Post
object falls out of scope or even explicitly unset with unset($post)
.
With help from WeakMap
, CommentRepository
class can now cache data (array of comments) without "holding onto" the Post
objects, but still associate the comments with Post
objects. The Post
object state is not changed (e.g. by setting $post->comments
property), and the life-cycle of the comments managed automatically alongside the garbage clearance of the Post
object itself.
WeakMap class synopsis
final class WeakMap implements ArrayAccess, Countable, IteratorAggregate, Traversable {
public function offsetGet(object $object);
public function offsetSet(object $object, mixed $value): void;
public function offsetExists(object $object): bool;
public function offsetUnset(object $object): void;
public function count(): int;
}
WeakMap keys must be objects
Weak Maps only allow objects as keys, and the associated allows arbitrary values.
Attempting to store any other data type as the key will throw a type error.
$map = new WeakMap();
$map['Foo'] = 'Bar';
// Fatal error: Uncaught TypeError: WeakMap key must be an object in ...:...
Appending to a WeakMap
is not allowed either.
$map = new WeakMap();
$map[] = 'Baz';
// Fatal error: Uncaught Error: Cannot append to WeakMap in ...:...
Error
on non-existing keys
If the requested object does not exist in the weakmap
object, an \Error
exception is thrown.
$map = new WeakMap();
$map[new stdClass()];
// Fatal error: Uncaught Error: Object stdClass#2 not contained in WeakMap in ...:...
This can be avoided by checking if the index exists in the map.
$map = new WeakMap();
isset($map[new stdClass()]); // false
WeakMap does not allow properties
$map = new WeakMap();
$map->foo = 'Bar';
// Fatal error: Uncaught Error: Cannot create dynamic property WeakMap::$foo in ...:...
WeakMap does not support Serialization/Unserialization
Weak Maps are not allowed to be serialize
d or unserialize
d. Given the WeakMap
class is declared final
, this can be changed either.
$map = new WeakMap();
serialize($map);
// Fatal error: Uncaught Exception: Serialization of 'WeakMap' is not allowed in ...:...
$serialized_str = 'C:7:"WeakMap":0:{}';
unserialize($serialized_str);
// Fatal error: Uncaught Exception: Unserialization of 'WeakMap' is not allowed in ...:...
Iterating Weak Maps
WeakMap
class implements Traversable
interface, it can be used iterated with foreach
construct.
Further, WeakMap
implements IteratorAggregate
that brings a WeakMap::getIterator
method.
$map = new WeakMap();
$obj1 = new stdClass();
$map[$obj1] = 'Object 1';
foreach ($map as $key => $value) {
var_dump($key); // var_dump($obj1)
var_dump($value); // var_dump('Object 1');
}
The iterator itself can be extracted with getIterator
method, and the return value is an iterable
.
$map = new WeakMap();
$obj1 = new stdClass();
$map[$obj1] = 'Object 1';
$iterator = $map->getIterator();
foreach ($iterator as $key => $value) {
var_dump($key); // var_dump($obj1)
var_dump($value); // var_dump('Object 1');
}
Practical Use Cases
Weak Maps can help with temporary storage of objects, but without affecting the memory usage due to the temporary storage itself.
When used as a cache storage, the object keys can be used to store a primitive state of the object, and reduce subsequent calls to an expensive function that is supposed to process or enhance the object.
Another use case would be storage of completion data for a collection of objects, but without mutating the object itself.
For more examples of Weak Maps in PHP 8.0) along with code examples, see PHP WeakMap Practical Use Cases.
Backwards Compatibility Impact
Weak Maps is a new feature in PHP 8.0, and unless there is a class with name WeakMap
in the global namespace, there should not be any issues when upgrading an existing code base to PHP 8.0.
Because the WeakMap
is an internal class, this functionality cannot be back-ported to prior PHP versions and make it work with the native garbage collector. A user-land implementation exists, but at a significant performance penalty.
A PECL extension named weakref
provided similar functionality, but it is no longer maintained.
Null-safe operator
Null-safe operator is a new syntax in PHP 8.0, that provides optional chaining feature to PHP.
The null-safe operator allows reading the value of property and method return value chaining, where the null-safe operator short-circuits the retrieval if the value is null
, without causing any errors.
The syntax is similar to the property/method access operator (->
), and following the nullable type pattern, the null-safe operator is ?->
.
$foo?->bar?->baz;
Null safe operator silently returns null
if the expression to the left side evaluates to null
.
class Customer {
public function getAddress(): ?Address {}
}
class Address {
public function getCountry(): string {}
}
$country = $customer->getAddress()->getCountry();
In the snippet above, the Customer::getAddress()
method return value is nullable; It can return a null
value, or an object of Address
class.
The $customer->getAddress()->getCountry()
chain is not "null safe", because the return value of getAddress
can be null
, and PHP throws an error when it tries to call getCountry()
method:
Fatal error: Uncaught Error: Call to a member function getCountry() on null in ...:...
To safely access the address, it was necessary to check the null
return values before further accessing the return value.
$address = $customer->getAddress();
$country = $address ? $address->getCountry() : null;
$address = $customer->getAddress();
if ($address) {
$country = $address->getCountry();
}
else {
$country = null;
}
The null-safe operator solves this by short-circuiting the property/method access, and returning null
immediately if the left side of the operator is null
, without executing the rest of the expression.
- $address = $customer->getAddress();
- $country = $address ? $address->getCountry() : null;
+ $country = $customer->getAddress()?->getCountry();
The ?->
null-safe operator can help reduce excessive isset()
and ternary checks.
Null-safe operator was added to PHP 8.0 between alpha 3 release and first beta release, right around the time PHP 8.0 reached its feature-freeze.
Read-Only
The null-safe operator is read-only. It cannot write/assign values from it.
class Customer {
private ?Address $address;
public function getAddress(): ?Address {
return $this->address;
}
}
class Address {
public string $country;
}
$customer->getAddress()?->country = 'NL';
This snippet attempts to write to the $country
property of the Address
object. This is not allowed with the null-safe operator.
Fatal error: Can't use nullsafe operator in write context in ... on line ...
Chaining
The null-safe operator can be chained, and the expression as a whole will be short-circuited from the first null-safe operator that encounters null.
$customer->getAddress()?->getCoordinates()->getLongitude()->format();
This chain of calls are null-safe. PHP will stop and immediately return null
if any of the null-safe return operators evaluate to null
.
{code-ok} Null-safe operator must be present at every step the chain can be short-circuited. Using the
?->
operator will not make the whole chain null-safe.
Evaluated from left to right
The null-safe operator is evaluated from left to right. Most importantly, it does not prioritize other function calls or other access patterns such as array-access.
<?php
class Customer {
public function getAddress(): ?Address {}
}
class Address {
public function setCountry(string $country_code): void {}
}
$customer->getAddress()?->setCountry(GeoIP::getCountry());
If the Customer::getAddress
method returns a valid Address
object, the GeoIP::getCountry()
will be executed, and passed to the Address::setCountry()
method call. Because the null-safe operators are evaluated from left to right, the GetIP::getCountry()
method will never be called if the Customer::getAddress()
method returns null
.
Note that using braces will make the expression inside the braces execute altogether, as opposed to the left-to-write pattern.
$customer->getAddress()?->setCountry((GeoIP::getAddress()?->getCountry()))
If Customer::getAddress()
returns a value other than null
, the (GeoIP::getAddress()?->getCountry())
chain will be executed, and the return value will be passed to the Address::setCountry()
call.
No References
The null-safe operator cannot be used with references.
$country = &$customer->getAddress()?->getCountry();
Fatal error: Cannot take reference of a nullsafe chain in ... on line ...
Backwards Compatibility Impact
Null-safe operator is a new syntax, and cannot be back-ported to older PHP versions.
Running code that uses null-safe operator will raise a parse error:
Parse error: syntax error, unexpected '->' (T_OBJECT_OPERATOR) in ... on line ...
`static` return type for class methods
PHP 8.0 allows static
as a return type for class methods.
class Foo {
public static function getInstance(): static {
return new static();
}
}
PHP class methods can return self
and parent
in previous versions, but static
was not allowed in PHP versions prior to 8.0. The newly allowed static
return type allows to narrow down the return type to the called class.
The static
return type helps classes with fluent methods (i.e the ones with return $this
), immutable classes (i.e return clone $this
) or static methods that return an instance of the class itself.
Without the static
type allowed in return types, one would have to use self
as the return type, which may not be the ideal one. PHP DocBlock already allowed @return static
in its DocBlocks to indicate that the methods return the object itself, or an instance of the same class.
With PHP 8.0's static
return type support, it is now possible to replace DocBlock @return static
statements with a return type declaration.
class Foo {
- /**
- * @return static
- */
- public static getInstance() {
+ public static getInstance(): static {
return new static();
}
}
Variance
static
return type follows Liskov Substitution Principle. A child class method can return a narrower class object than the parent methods return type.
Because static
always refer to the class name of the called object (i.e. same as get_class($object)
), static
is a subset of self
, which in turn is subset of parent
.
It is possible for a child class to return static
, even if the parent methods return type is self
, or parent
.
For example, the entirety of the following inheritance is valid:
Excessive inheritance chains are almost always a bad idea, and leads to unmanageable code. The snippet below is just for demonstration.
class Foo {
public function getInstance(): mixed {}
}
class Bar extends Foo {
public function getInstance(): object|null {}
}
class Baz extends Bar {
public function getInstance(): object {}
}
class Qux extends Baz {
public function getInstance(): parent {}
}
class Quux extends Qux {
public function getInstance(): self {}
}
class Corge extends Quux {
public function getInstance(): static {}
}
The snippet above uses other PHP 8.0 features
Attempting to "widen" the return type scope with self
, parent
, or any other return type will cause error:
class Corge {
public function getInstance(): static {}
}
class Grault extends Corge {
public function getInstance(): parent {}
}
Fatal error: Declaration of Grault::getInstance(): Corge must be compatible with Corge::getInstance(): static in ... on line ...
In fact, it is also not allowed to replace the static
return type with the class name of the child class or parent class either.
Return type only
static
return type is only allowed as a return type. It is not allowed as a property type or a parameter type.
This is because the static
return type is always narrowing the scope, which is not allowed in typed properties and parameter types.
Not allowed outside class context
Only class methods can declare static
return type. Standard functions or closures are not allowed to declare static
return type.
function get_instance(): static {}
Fatal error: Cannot use "static" when no class scope is active in ... on line ...
Backwards Compatibility Impact
Code with static
return type will not be backwards-compatible with older PHP versions prior to 8.0. Doing so will result in a parse error:
Parse error: syntax error, unexpected 'static' (T_STATIC) in ... on line ...
It is not possible to back-port this feature to older PHP versions either.
New `get_resource_id` function
PHP resources, such as Curl handlers, open files, database connections, can be cast to int
. PHP 8 adds a new get_resource_id
function that is essentially a (int) $resource
cast to make it easier to retrieve the resource ID.
The advantage of the new get_resource_id
function is the type safety, that arguments and return type are checked to be a resource
and an int
.
get_resource_id()
Function synopsis
function get_resource_id($res): int {}
Note that PHP does not have a resource
type that can be enforced yet.
Polyfill
function get_resource_id($res): int {
if (!\is_resource($res) && null === @get_resource_type($res)) {
throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));
}
return (int) $res;
}
From Symfony/Polyfill
package. Supports PHP 7.0 and later.
Backwards Compatibility Impact
get_resource_id
is a new function, and it can be easily polyfilled in previous PHP versions. Unless you have declared a function with exact same name in the global namespace, there should not be any problems.
New `str_contains` function
One of the usability improvements that comes with PHP 8.0 is the new str_contains
function. As the name suggests, it checks if the given haystack string contains a given string needle.
Without this function, the usual way to find if a given string contains another string is to use to the strpos()
function:
if (strpos('Foo Bar Baz', 'Bar') !== false) {
echo 'Found';
}
strpos()
function returns the position of the needle string, or false
if the needle is not found. This is error-prone, because if the needle is found at the position 0
of the haystack, it evaluates to false
unless strict comparison (===
) used.
To explain further, the following snippet is not correct:
if (strpos('Foo Bar Baz', 'Foo')) {
echo 'Found';
}
Because Foo
is found at the beginning of the haystack, the return value of strpos()
call will be 0
, which evaluates to false, and the if
block will not run.
With the new str_contains
function, it is easy to do this:
if (str_contains('Foo Bar Baz', 'Foo')) {
echo 'Found';
}
Case sensitivity
str_contains()
function is case-sensitive. There is no case-insensitive variant of this function. There is no technical reason to not have a case-insensitive str_icontains()
function, but there isn't one for now to keep things simple.
Multi-byte strings
For strpos()
function, there is a multi-byte safe mb_strpos()
variant. However, for str_contains()
function, there is no mb_str_contains()
function. This is because internally, PHP strings are streams of bytes, and an mb_str_contains()
function will be identical to the functionality of str_contains()
as it would be checking for a sequence of bytes in another sequence of bytes anyway.
Conflicts with user-land implementations
There are several user-land implementations of this functionality, often with the exact same name. As of this moment, there are over 192K str_contains()
matches on Github, and over 6K search results of str_contains()
function declarations.
Most notably, Laravel offers a helper function str_contains()
, but this function accepts an array of needles for the second parameter as well, which is not compatible with PHP core implementation.
Empty strings
If you search for an empty needle (""
), PHP will always return true
. To quote Nikita:
As of PHP 8, behavior of '' in string search functions is well defined, and
we consider '' to occur at every position in the string, including one past
the end. As such, both of these will (or at least should) return true. The
empty string is contained in every string.
This means the following will always returns true
:
str_contains('Foo', ''); // true
str_contains('', ''); // true
Polyfills
A PHP 7.0+ compatible polyfill is straight forward and simple:
if (!function_exists('str_contains')) {
function str_contains(string $haystack, string $needle): bool {
return '' === $needle || false !== strpos($haystack, $needle);
}
}
Backwards compatibility impact
str_contains()
is a new function; Unless you already have a str_contains()
function declared, there should be no BC impact.
Most user-land implementations only declare its own str_contains()
function only if that function does not exists. You might run into obscure bugs if the user-land implementations are incompatible with PHP core's. See the section above about user-land implementations.
New `fdiv` function
PHP 8 adds a new fdiv()
function to fit along functions such as fmod()
and intdiv()
.
As of now, attempting a divide a number by zero results in inconsistent behavior.
Division operator triggers warnings
$num = 1 / 0;
// Warning: Division by zero in ... on line ...
intdiv()
function throws DivisionByZeroError
exceptions
$num = intdiv(1, 0);
// Fatal error: Uncaught DivisionByZeroError: Division by zero in ...:...
Modulo operator throws DivisionByZeroError
exceptions
$mod = 1 % 0;
// Fatal error: Uncaught DivisionByZeroError: Modulo by zero in ...:...
Semantically, the division operator should throw a DivisionByZeroError
exception as well, but due to backwards compatibility issues, it is still a warning.
In an attempt to streamline this, Nikita suggested to add the new fdiv()
function.
This function will follow the IEEE-754 standard on Floating-Point Arithmetic, and return NAN
, or ±INF
as float. The return value of fdiv
will be identical to the current division operator's behavior.
## +INF
fdiv(1, 0); // float(INF)
$num = 1 / 0; // float(INF)
// Warning: Division by zero in ... on line ...
## -INF
fdiv(-1, 0); // float(-INF)
$num = -1 / 0; // float(-INF)
// Warning: Division by zero in ... on line ...
The division operator will eventually throw DivisionByZeroError
when this function is well-established in the future.
Polyfill
A PHP 7.0+ polyfill that mimics the new fdiv()
function:
if (!function_exists('fdiv')) {
function fdiv(float $dividend, float $divisor): float {
return @($dividend / $divisor);
}
}
Backwards compatibility impact
fdiv()
is a new function. A quick GitHub search did not reveal any positive results for existing code that declares a function with the same name.
It is recommended that you use this function whenever possible (see the polyfill above) because division by zero using division operator is already triggering a PHP warning in your existing code, which will escalate to a DivisionByZeroError
exception in the future.
New `get_debug_type` function
get_debug_type()
function that comes with PHP 8.0 is an attempt to improve gettype()
function which returns inconsistent values and does not expand to reveal the class names of objects.
One of the use cases of this function is to use it as an easy way to explain an unexpected variable in error logs or exception messages because the return type of gettype()
is often not verbose enough to be meaningful.
get_debug_type()
vs gettype()
The following sections will explain the differences between the new get_debug_type()
function and current gettype()
function return values.
Scalar types
Type | Example value | gettype() | get_debug_type() |
---|---|---|---|
String | "Foo" |
string |
string |
Arrays | [1, 2] |
array |
array |
Null | null |
NULL |
null |
Integers | 123 |
integer |
int |
Float | 3.141 |
double |
float |
Boolean | true |
boolean |
bool |
Boolean | false |
boolean |
bool |
Notice how the new get_debug_type()
function returns the exact types that you use in scalar typing.
Class objects and anonymous functions
Type | Example value | gettype() | get_debug_type() |
---|---|---|---|
Class object | new stdClass() |
object |
stdClass |
Class object | new DateTime() |
object |
DateTime |
Class object | new Foo\Bar() |
object |
Foo\Bar |
Closure | function() {} |
object |
Closure |
Anonymous class | new class {} |
object |
class@anonymous |
Anonymous subclass | new class extends Foo{} |
object |
Foo@anonymous |
As you can see, get_debug_type()
is starting to get more helpful here, because it helpfully reports the name of the class, or further details about anonymous classes.
If a closure is passed (which is internally an object), get_debug_type()
returns it as Closure
.
When an anonymous class object is passed, it returns class@anonymous
for classes without a parent/interface, or parent/interface name, followed by @anonymous
.
This is where the get_debug_type()
function gets quite helpful because instead of calling get_class()
and get_parent_class()
, get_debug_type()
directly returns a useful name for the given object. This can help minimize the boilerplate code and code bloat in when exceptions are thrown and errors are logged.
Resources
Type | Example value | gettype() | get_debug_type() |
---|---|---|---|
Streams | tmpfile() |
resource |
resource (stream) |
Curl handler | curl_init() |
resource |
resource (curl) |
Closed Curl handler | curl_close($ch) |
resource (closed) |
resource (closed) |
XML document | xml_parser_create() |
resource |
resource (xml) |
... | ... | resource |
resource (TYPE) |
For all resource types in PHP, get_debug_type()
function will return the type of that resource too.
Use cases
When you want to output a verbose error message on an unexpected type, instead of doing an ugly type lookup, which often overlooks other variable types, you can now easily get the type of a passed variable:
Previously:
if (!($foo instanceof Foo)) {
throw new TypeError(
sprintf(
'Parameter 1 is expected to be of type "%s", got "%s" instead.',
Foo::class,
(is_object($foo) ? get_class($foo) : gettype($foo))
)
);
}
Now:
if (!($foo instanceof Foo)) {
throw new TypeError(
sprintf(
'Parameter 1 is expected to be of type "%s", got "%s" instead.',
Foo::class,
get_debug_type($foo)
)
);
}
Polyfill
if (!function_exists('get_debug_type')) {
function get_debug_type($value): string {
switch (true) {
case null === $value: return 'null';
case \is_bool($value): return 'bool';
case \is_string($value): return 'string';
case \is_array($value): return 'array';
case \is_int($value): return 'int';
case \is_float($value): return 'float';
case \is_object($value): break;
case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
default:
if (null === $type = @get_resource_type($value)) {
return 'unknown';
}
if ('Unknown' === $type) {
$type = 'closed';
}
return "resource ($type)";
}
$class = \get_class($value);
if (false === strpos($class, '@')) {
return $class;
}
return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';
}
}
Above is a polyfill that you can use in your code if you want to bring the get_debug_type()
function to your own code that cannot require PHP 8.0 to run. This snippet is from Symfony's PHP 8.0 polyfill.
Backwards compatibility impact
get_debug_type()
is a new function. Unless you have declared a function with an identical name in the global namespace, this should not bring any BC issues.
New `preg_last_error_msg` function
Some of the legacy PHP functionality does not throw exceptions on errors, and it is up to the caller to check if an operation was successful or not.
When you run a regular expression using PHP's preg_
functions, these functions do not throw an exception if something went wrong. It is up to the caller to retrieve any error messages using preg_last_error
function, which returns an error code if there were any errors.
PHP's core json_encode()
and json_decode()
functions follow a similar pattern, and unless the exception behavior is explicitly requested (PHP 7.3+), you have to call json_last_error
function to retrieve any errors occurred during the last operation.
However, JSON functionality comes with a json_last_error_msg()
function to retrieve the human-friendly error message of the JSON encode/decode error.
PREG functionality did not come with a function to retrieve the human-friendly error message. PHP 8.0 brings one!
The new preg_last_error_msg()
returns a human-friendly error message, or "No error"
(as string) if there were no errors.
preg_match('/(?:\D+|<\d+>)*[!?]/', 'foobar foobar foobar');
var_dump(preg_last_error()); // 2
With the new preg_last_error_msg()
function, you can directly get the error message:
preg_match('/(?:\D+|<\d+>)*[!?]/', 'foobar foobar foobar');
var_dump(preg_last_error()); // 2
var_dump(preg_last_error_msg()); // Backtrack limit was exhausted
Polyfill
The new preg_last_error_msg()
function simply returns a human-friendly text error message instead of the error code. This can be polyfilled with a hardcoded list of error codes mapped to error messages:
if (!function_exists('preg_last_error_msg')) {
/**
* Returns the error message of the last PCRE regex execution.
* @return string
*/
function preg_last_error_msg(): string {
switch (preg_last_error()) {
case PREG_NO_ERROR:
return 'No error';
case PREG_INTERNAL_ERROR:
return 'Internal error';
case PREG_BACKTRACK_LIMIT_ERROR:
return 'Backtrack limit exhausted';
case PREG_RECURSION_LIMIT_ERROR:
return 'Recursion limit exhausted';
case PREG_BAD_UTF8_ERROR:
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
case PREG_BAD_UTF8_OFFSET_ERROR:
return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
case PREG_JIT_STACKLIMIT_ERROR:
return 'JIT stack limit exhausted';
default:
return 'Unknown error';
}
}
}
Backwards compatibility impact
preg_last_error_msg()
is a new function, and unless you have declared a function with the same name in global namespace, there should be no BC issues.
`::class` magic constant is now supported on objects
PHP has a magic constant ::class
that resolves a class name to its fully-qualified class name. When used with a class name, use
and use as
statements will be resolved, or the current namespace will be prefixed which makes it a fully-qualified class name.
Example:
namespace App\Demos;
use Foo\Bar;
use Bar\Baz as BBaz;
class Demo {}
// `use` statement is resolved:
echo Bar::class; // "Foo\Bar"
// `use` X `as` Y is resolved:
echo BBaz::class; // "Bar\Baz"
// Current namespace is resolved:
echo Demo::class; // "App\Demos\Demo"
Until PHP 8.0, the ::class
magic constant was not allowed on objects.
$object = new Foo\Bar();
echo $object::class;
// Fatal error: Cannot use ::class with dynamic class name.
With PHP 8.0, now you can use ::class
constant on objects, and it will be correctly resolved at run time:
$object = new Foo\Bar();
echo $object::class;
// PHP 8.0+:
// "Foo\Bar"
Same result as get_class()
The ::class
constant on an instantiated object will return the exact same return value as a get_class()
call.
get_class($object) === $object::class;
Non-objects are not allowed
Using ::class
on a non-object is not allowed:
$object = array();
$object::class;
// Fatal error: Uncaught TypeError: Cannot use ::class on value of type array in ...:...
If you need to get the type of any variable, PHP 8.0 comes with a new get_debug_type()
function that you can call to get the class name, scalar types, resource type, etc all from one handy function.
Backwards compatibility impact
In PHP versions prior to 8.0, using ::class
on an object triggers a fatal error. Because PHP 8.0 is relaxing this error, this should not create any new BC issues. However, this will make your code require PHP 8.0 to run without a way to polyfill this functionality to older versions.
New `ValueError` Error Exception
\ValueError
is a new Exception type that extends \Exception
class, and from PHP 8.0, you will be seeing lots of them! \ValueError
exception is thrown when a value encountered is of correct type, but it cannot be used to continue the operation.
What about other exceptions?
PHP already has exceptions such as \InvalidArgumentException
, \OutOfRangeException
and \LengthException
exceptions that convey a more precise error message.
However, exceptions is the keyword here: The new \ValueError
exception extends \Error
, instead of \Exception
. While you can throw \ValueError
exceptions in user-land code, PHP core functions will throw \ValueError
exceptions except for a few specific cases (such as sprintf()
function throwing \ArgumentCountError
exceptions instead of the legacy Warning: sprintf(): Too few arguments
warning).
Throwable
├── Error
│ ├── TypeError
│ ├── ValueError
│
└── Exception
├── LogicException
├── DomainException
├── InvalidArgumentException
├── LengthException
└── OutOfRangeException
This is a simplified chart of PHP core \Error
and \Exception
. You can take a look at full PHP Exception hierarchy in this post.
From PHP 8.0 and forward, \ValueError
errors will be thrown when the value passed to a function is of a valid type, but is not valid for the operation.
Examples
-
strpos()
attempting to set a offset longer than the haystack lengthBefore PHP 8.0:
$a = strpos("s", "small", 16); // Warning: strpos(): Offset not contained in string in ... on line ... var_dump($a); // bool(false)
From PHP 8.0
$a = strpos("s", "small", 16); // Uncaught ValueError: Offset not contained in string in ...:...
-
range()
with non-positive stepsBefore PHP 8.0:
$a = range(1, 2, 0); // Warning: range(): step exceeds the specified range in ... on line ... var_dump($a); // bool(false)
From PHP 8.0
$a = range(1, 2, 0); // Uncaught ValueError: Step exceeds the specified range ...:...
-
array_rand()
with an empty arrayBefore PHP 8.0:
$a = array_rand(array(), 0); // Warning: array_rand(): Array is empty in ... on line ... var_dump($a); // NULL
From PHP 8.0
$a = array_rand(array(), 0); // Uncaught ValueError: Array is empty in ...:...
But why?
The new \ValueError
exception is introduced as part of the major change Internal function warnings now throw TypeError
and ValueError
exceptions, where you can find detailed information.
Polyfill
It is possible to polyfill this exception class by simply declaring a user-land class with the same name.
if (!class_exists('\ValueError')) {
class ValueError extends \Error {
}
}
Note that this will not make internal PHP functions throw \ValueError
exceptions when appropriate. However, if you have user-land code that needs to throw \ValueError
exceptions, it is now possible with the polyfill.
Backwards compatibility impact
\ValueError
is a new class, and unless you have user-land implementations, there should be no BC impact.
New `PhpToken` Tokenizer class
token_get_all
function from tokenizer
extension parsers a given PHP source code, and returns an array of tokens. Its array return value is not quite convenient to use.
PHP 8.0 comes with a new class PhpToken
that provides an object-oriented approach to tokenizer results.
$code = "<?php echo "Hello world"; ?>";
$tokens = PhpToken::tokenize($code);
Prior to PHP 8.0 rc4, the
PhpToken::tokenize()
method was calledPhpToken::getAll()
.
The return value is an array of PhpToken
objects, that provides more fluent methods and public properties to retrieve information about each token.
PhpToken
class synopsis
class PhpToken implements Stringable {
/**
* One of the T_* constants, or an integer < 256 representing a single-char token.
*/
public int $id;
/** The textual content of the token. */
public string $text;
/** The starting line number (1-based) of the token. */
public int $line;
/** The starting position (0-based) in the tokenized string. */
public int $pos;
/** @return static[] */
public static function tokenize(string $code, int $flags = 0): array {}
final public function __construct(int $id, string $text, int $line = -1, int $pos = -1) {}
/**
* Whether the token has the given ID, the given text,
* or has an ID/text part of the given array.
*/
public function is(int|string|array $kind): bool {}
/** Whether this token would be ignored by the PHP parser. */
public function isIgnorable(): bool {}
/** Get the name of the token. */
public function getTokenName(): ?string {}
/** Returns $text property */
public function __toString(): string {}
}
Related Changes from the snippet
Each returned PhpToken
object will contain public properties $token->id
, $token->text
, $token->line
, and $token->pos
to retrieve information about the token.
$token->id
: One of theT_*
constants, or an integer < 256 representing a single-char token.$token->text
: The textual content of the token.$token->line
: The starting line number (1-based) of the token.$token->pos
: The starting position (0-based) in the tokenized string.
The methods $token->is()
, $token->getTokenName()
, $token->isIgnorable()
, and $token->__toString()
can return additional information as well.
PhpToken::is(int|string|array $kind): bool
: Returns whether the token is of a givenT_
token (integer), a string token, or an array ofint|string
tokens to match against the token ID.$token->getTokenName
: The textual content of the token.$token->isIgnorable()
: Whether this token would be ignored by the PHP parser.$token->__toString()
: Returns the$token->text
value.
Backwards Compatibility Impact
PhpToken
is a new class added in PHP 8.0, and unless there is no such class in user-land PHP code, there should not any upgrading issues.
The functionality can be backported to other PHP versions as well. See phpwatch/phptoken-polyfill
New `str_starts_with` and `str_ends_with` functions
PHP 8.0 comes with two new functions to help you easily assert if a given string is present at the beginning or ending of a haystack string. This goes nicely with str_contains()
in PHP 8.0.
str_starts_with()
: Check if a given haystack string starts with the given needle stringstr_ends_with
: Check if a given haystack string ends with the given needle string
str_starts_with(string $haystack, string $needle): bool;
str_ends_with(string $haystack, string $needle): bool;
PHP 8 finally brings the total number of string-related functions in PHP to over 100, and the new functions can be easily mimicked with existing functions such as strpos
, substr
, strncmp
, and substr_compare
. However, these new functions were well-received due to possible engine-level optimizations and their frequent use-cases.
Case sensitivity
Both str_starts_with()
and str_ends_with()
functions are case-sensitive. There are no flags or other functions to make them case-insensitive. This is the same pattern with str_contains
Multi-byte strings
Multi-byte (mb_*
) variants for str_starts_with()
and str_ends_with()
are not currently planned.
Empty strings
Similar to str_contains
, PHP now considers empty string (""
) to be present in everywhere in a string. To quote Nikita:
As of PHP 8, behavior of '' in string search functions is well defined, and
we consider '' to occur at every position in the string, including one past
the end. As such, both of these will (or at least should) return true. The
empty string is contained in every string.
The following calls will be always true:
str_starts_with('Foo', ''); // true
str_starts_with('', ''); // true
str_ends_with('Foo', ''); // true
str_ends_with('', ''); // true
Polyfills
Here is a polyfill for PHP 7.0 and later. Be sure to wrap them with function_exists()
calls where you use them. These functions pass the exact same tests in PHP core.
function str_starts_with(string $haystack, string $needle): bool {
return \strncmp($haystack, $needle, \strlen($needle)) === 0;
}
function str_ends_with(string $haystack, string $needle): bool {
return $needle === '' || $needle === \substr($haystack, - \strlen($needle));
}
Conflicts with current user-land implementations
Starts-with and ends-with functionality is often provided as helper functions in various frameworks. This includes Symfony String component, Laravel Str
helper, and Yii StringHelper
.
There are over 4,000 str_starts_with()
matches on GitHub, for PHP, most of which appear to be already namespaced.
Case-insensitivity support | Empty strings at every position | |
---|---|---|
PHP 8.0 str_starts_with str_ends_with |
No | Yes |
Polyfill (above) str_starts_with str_ends_with |
No | Yes |
Symfony String ::startsWith ::endsWith |
Yes With ::ignoreCase() |
No |
Laravel Str::startsWith Str::endsWith |
No | No |
Yii StringHelper::startsWith StringHelper::endsWith |
Yes (default) With parameter |
No |
Backwards compatibility impact
Both str_starts_with
and str_ends_with
functions are new functions. Unless you already have a str_contains()
function declared, there should be no BC impact.
PHP 8's new behavior that it considers there is an empty string at every position of a string can be tricky. Note that Laravel helpers and Symfony String component, among many others return false
when you search for an empty string needle (""
) at the start and end of strings, although PHP core returns true
.
New `mixed` pseudo type
mixed
is a pseudo type added in PHP 8 that conveys the type of the parameter/return/property can be of any type. mixed
type includes all scalar types in PHP, null
, all class objects, callable
, and even resource
.
mixed
is equivalent to a Union Type of:
string|int|float|bool|null|array|object|callable|resource
With mixed
, it is now possible to declare mixed
as the type when the parameters, returns, and class properties can be of any type.
class Example {
public mixed $exampleProperty;
public function foo(mixed $foo): mixed {}
}
mixed
is a pseudo type
mixed
represents any type PHP can handle, and thus you cannot cast a variable to mixed
because it simply doesn't make sense.
$foo = (mixed) $bar;
Further, there is no is_mixed()
function for the same reasons.
gettype()
and get_debug_type()
functions will never return mixed
as the type of a variable either.
mixed
in union with other types
Because mixed
represents all types, mixed
cannot be used in union with other types:
function (mixed|FooClass $bar): int|mixed {}
Both union types above are not allowed, and will result in a fatal error:
Fatal error: Type mixed can only be used as a standalone type in ... on line ...
mixed
is assumed when no type is declared
When a function parameter or a class property has no explicit type declared, the type is now assumed to be mixed
.
Be mindful when you add
mixed
type to all your existing code; PHP 8 has Union Types that might be a better fit because Union Types allow you to be more specific.
For return types, lack of an explicit return type is equal to mixed|void
.
However, note that you cannot declare
mixed|void
as a return type becausemixed
is not allowed in a Union Type.
Type variance
When a class method, return type, or a property type is overridden by a sub-class, Liskov Substitution Principle is respected.
Contravariance: mixed
parameters types
Function parameter types can be "widened" at a child class or an interface implementation because the widened type still fulfills the contract of the interface/parent class. This means child class parameters can declare a type with a Union Type that includes more types, or a class name that is a parent of the current class.
When a parameter type is declared as mixed
, this practically prevents further contravariance because mixed
includes all types PHP works with. If possible, always opt for more specific types because once you mark a parameter type as mixed
in a public API, all child classes must be capable to deal with mixed types.
Covariance: mixed
return types
If a parent class method has declared a return type other than mixed
, child classes will not be allowed to declare mixed
because it widens the return type scope, thus breaking LSP.
class A {
public function foo(): mixed {}
}
class B extends A{
public function foo(): void {}
}
The above will result in a fatal error:
Fatal error: Declaration of B::foo(): void must be compatible with A::foo(): mixed
This is because mixed
does not include void
type. If a return type is not explicitly declared, it is assumed to be mixed|void
.
All following declarations are allowed:
class A {
public function foo() {}
public function bar() {}
public function baz(): mixed {}
}
class B extends A{
public function foo(): mixed {}
public function bar(): void {}
public function baz(): string {}
}
B::foo
: Allowed: Narrows down the assumedmixed|void
return type ofA::foo
.B::bar
: Allowed: Narrows down the assumedmixed|void
return type ofA::bar
.B::baz
: Allowed: Narrows down the declaredmixed
type.
Invariance: mixed
property types
If a property type is declared as mixed
, this type cannot be omitted or changed at all.
Usage with void
PHP supports void
pseudo return type to indicate that the function will not return anything. This is equivalent to lack of a return
statement, or return;
expression without setting an explicit value.
void
type and mixed
cannot be in a union. Further mixed
does not include void
.
Nullable mixed
types
It is not allowed to declare mixed
type as nullable because mixed
includes null
.
All of the following declarations are not allowed:
function foo(mixed|null $foo) {}
function foo(?mixed $foo) {}
function foo($foo): mixed|null {}
function foo($foo): ?mixed {}
All declarations above will raise a fatal error:
Fatal error: Type mixed can only be used as a standalone type in ... on line ...
Practical Usage
Many PHP internal functions accept various types, which can now be declared with the mixed
type. However, for user-land functions, it is often better to use a specific type or a Union Type.
Functions such var_dump
or get_debug_type()
can declare its parameters as mixed
because these functions accept any type by definition.
If you declare a parameter/return type/class property as mixed
type, be mindful that mixed
includes types such resource
and callable
, which are not easily stored, serialized, sanitized, or displayed.
Most user-land functions that need to "accept anything", such as logging functions are better off with a Union Type such as string|int|float|bool|null|object|array
.
Backwards compatibility
mixed
is soft-reserved since PHP 7. Until PHP 8, it is technically possible to declare a class with name mixed
, and it will not raise any errors, warnings, or notices. PHPDoc standard widely used mixed
as a type declaration, so it is highly unlikely that even the wildest code base out there declares a class with name mixed
.
Attempting to declare a class with name mixed
in PHP 8 results the following error:
Fatal error: Cannot use 'mixed' as class name as it is reserved in ... on line ...
Polyfill
It is not possible to polyfill this functionality because it's an internal type. If you use mixed
type anywhere in your code, be sure that it will always run on a PHP 8+ platform.
Related Changes
New `p` date format for UTC `Z` time zone designation
There is a new date format introduced in PHP 8.0: p
. This new lower case "p" date format behaves similar to the upper case P
, which shows the time zone offset. The difference between P
and p
format is that the new p
format uses Z
for UTC time, while the P
format uses +00:00
.
The ISO 8601 date format permits UTC time with +00:00
format, or with Z
. This means both date formats below are valid:
- 2020-09-09T20:42:34+00:00
- 2020-09-09T20:42:34Z
With the new p
date formatter, it is now possible to create a date with the second pattern above as well:
date('Y-m-d\TH:i:sp');
Here is a quick list of PHP ISO8601-related date formats:
<PHP 8.0 | >=PHP 8.0 | |
---|---|---|
DateTimeInterface::ATOM DATE_ATOM Valid ISO 8601 date format. |
2020-09-09T20:42:34+00:00 | 2020-09-09T20:42:34+00:00 |
c Valid ISO 8601 date format. |
2020-09-09T20:42:34+00:00 | 2020-09-09T20:42:34+00:00 |
P |
+00:00 | +00:00 |
p p is new in PHP 8.0 |
p | Z |
Y-m-d\TH:i:sP Valid ISO 8601 date format. Same as c and DATE_ATOM |
2020-09-09T20:42:34+00:00 | 2020-09-09T20:42:34+00:00 |
Y-m-d\TH:i:sp p is new in PHP 8.0 |
2020-09-09T20:42:34p | 2020-09-09T20:42:34+00:00 |
Dates formatted with +00:00
to indicate UTC times are perfectly valid. Quoting from Wikipedia:
An offset of zero, in addition to having the special representation "Z", can also be stated numerically as "+00:00", "+0000", or "+00".
Backwards Compatibility Impact
UTC offsets indicated with +00:00
is perfectly valid ISO 8601 date formats. If it is absolutely necessary to use Z
instead of of +00:00
for UTC time, the new p
format can be helpful.
While it is not possible to backport the formatter itself, a simple string-replace hack could achieve the same results as PHP 8.0 in all PHP versions since 5:
str_replace('+00:00', 'Z', date('c'));
Match Expressions
Match expression syntax is one of the nicest features in PHP 8 that improves the switch
syntax in multiple ways.
$status = match($request_method) {
'post' => $this->handlePost(),
'get', 'head' => $this->handleGet(),
default => throw new \Exception('Unsupported'),
};
Functionality from the match
expression above, compared to a switch
block:
- switch ($request_method) {
+ $status = match($request_method) {
- case 'post':
- $status = $this->handlePost();
- break;
+ 'post' => $this->handlePost(),
- case 'get':
- case 'head':
- $status = $this->handleGet();
- break;
+ 'get', 'head' => $this->handleGet(),
- default:
- throw new \Exception('Unsupported');
+ default => throw new \Exception('Unsupported'),
- }
+ };
match
expressions can return a value
The return value of the expression used in each "arm" (similar to each case
in switch
blocks) is can be assigned to a variable.
$name = match(2) {
1 => 'One',
2 => 'Two',
};
echo $name; // "Two"
It is not necessary to assign the return value to anything, the return value of the matched arm will be returned from the match
expression.
Multiple matching conditions allowed
It is possible for a match
expression to contain one or more matching conditions, and they will behave similar to multiple cascading case
keys in a switch
block.
match($request_method) {
'post' => $this->handlePost(),
'get', 'head' => $this->handleGet(),
};
$request_method === 'get'
and $request_method === 'head'
both conditions will be handled with $this->handleGet()
.
Each matching case must only contain one expression
Unlike switch
blocks that can contain any number of expressions, a match
arm can only contain only one expression.
match($name) {
'foo' =>
initFoo();
processFoo();
};
Syntax above is not allowed. Each arm must contain only a single expression.
Implicit break
Each matched "arm" of a match
expression only allows a single expression, and it will not fall-through, as it does in a switch
block.
switch ('test') {
case 'test':
$this->sendTestAlert();
case 'send':
$this->sendNuclearAlert();
}
It is easy to overlook the missing break
call in each of the switch
case
, which allows the code to fall-through to the next case. In the switch
block above, missing break;
statement makes the code fall-through and execute $this->sendNuclearAlert()
as well, although it is unlikely the outcome you expect.
match ('test') {
'test' => $this->sendTestAlert(),
'send' => $this->sendNuclearAlert(),
};
match
expressions work without explicit break
statements. It only executes one matching arm, and immediately returns the value, making it imply a break
call right after the expression the matched arm executes.
default
case
match
statement supports a default
arm that will work similar to the default
case in switch
blocks.
A default
arm will catch all expressions if none of the other conditions matched.
match ('Qux') {
'foo' => ...,
'bar' => ...,
default => echo 'Unknown: ' . $name,
};
// "Unknown: Qux"
match
expression MUST match a condition
switch
blocks silently proceeds the code flow if there are no matching case
keys. match
expressions do not.
In a match
expression, there must be condition that matches the expression, or a default
case to handle it. If there are no matches, match
expression throws an \UnhandledMatchError
exception.
$value = 3;
match($value) {
1 => 'One',
2 => 'Two',
};
match
expression above throws error:
Fatal error: Uncaught UnhandledMatchError in ...
UnhandledMatchError
exception
match
expressions throw an \UnhandledMatchError
exception if there are no matches in within the expression.
\UnhandledMatchError
is a new exception class in PHP 8, and it extends \Error
. For a full hierarchy of all PHP core exception classes, including the ones added in PHP 8, see Hierarchy of PHP exceptions
This class can be easily poly-filled:
class UnhandledMatchError extends \Error {}
Strict matches without type coercion
One of the most important design choices in match
expression is that it matches without type coercion.
function read(mixed $key): string {
return match ($key) {
1 => 'Integer 1',
'1' => 'String 1',
true => 'Bool true',
[] => 'Empty array',
[1] => 'Array [1]',
};
}
read(1); // "Integer 1"
read('1'); // "String 1"
read(true); // "Bool true"
In a typical switch
block, its cases are matched loosely, i.e with ==
. In match
expressions, all matching arms are matched with strict comparison (===
), leaving possible bugs in switch
blocks out.
In the snippet above, each individual arm will be matched for the value and type.
Match against arbitrary expressions
match
expression allows a given value to be matched against an expression.
match($foo){
404 => 'Page not found',
Response::REDIRECT => 'Redirect',
$client->getCode() => 'Client Error',
$response->getCode() => 'Response Error',
default => 'Unknown error'
};
The expressions will be evaluated in the order they are laid out.
match
expression will try to match $foo
against in this order:
$foo === 404
$foo === Response::REDIRECT
$foo === $client->getCode()
$foo === $response->getCode()
default
If it finds a positive match, the rest of the code will not be evaluated.
match
vs switch
switch |
match |
|
---|---|---|
Requires PHP ^8.0 |
No | Yes |
Returns value | No | Yes |
default condition support |
Yes | Yes |
Multiple conditions in single arm | Yes | Yes |
Multiple expressions in code block | Yes | No |
Implicit break |
No | Yes |
Falls-through without break |
Yes | No |
Throws an exception on no matches | No | Yes |
Match against arbitrary expressions | Yes | Yes |
Strict type-safe comparison | No | Yes |
Backwards compatibility impact
match
expressions are a new syntax in PHP 8. Code that uses match
expressions will not work in older PHP versions.
The \UnhandledMatchError
exception class can be backported.
Trying to run code that uses this expression will fail with a parse error in older PHP versions:
Parse error: syntax error, unexpected '=>' (T_DOUBLE_ARROW) in ... on line ...
New `DateTime/DateTimeImmutable::createFromInterface()` methods
PHP's DateTime
and DateTimeImmutable
classes have methods to create one instance from another.
DateTime::createFromImmutable(DateTimeImmutable $object): DateTime
DateTimeImmutable::createFromMutable(DateTime $date): DateTimeImmutable
Both DateTime
and DateTimeImmutable
classes implement DateTimeInterface
DateTimeInterface
├── DateTime
└── DateTimeImmutable
In PHP 8.0, both DateTime
and DateTimeImmutable
classes get a new method ::createFromInterface
. They behave exactly as they would in createFromMutable
and createFromImmutable
methods, but the new createFromInterface
method, as its name suggests, accepts any DateTimeInterface
implementation.
DateTime::createFromInterface(DateTimeInterface $object): DateTime
DateTimeImmutable::createFromInterface(DateTimeInterface $object): DateTimeImmutable
Backwards compatibility impact
This is a rather simple improvement in PHP 8.0, and it wouldn't be too difficult to mimic this functionality with instanceof
checks and calling either of the createFrom*
methods available in all PHP versions.
Libraries that extend DateTime
and DateTimeImmutable
can polyfill this functionality with a bit of trait-foo, using a trait
that conditionally declares the createFromInterface
method if it's not available in PHP version it runs on.
class MyCustomDateTime {
use DateTimeCreateFromInterfacePolyfill;
// ...
}
class MyCustomDateTimeImutable {
use DateTimeImmutableCreateFromInterfacePolyfill;
// ..
}
In the appropriately named files (for PSR-4 naming and autoloading), it is now possible to declare the two polyfill traits that will only contain the methods in PHP versions prior to 8.0
// path/to/DateTimeCreateFromInterfacePolyfill.php
if (PHP_VERSION_ID < 80000):
trait DateTimeCreateFromInterfacePolyfill {
public function createFromInterface(\DateTimeInterface $object): \DateTime {
if ($object instanceof \DateTimeImmutable) {
return \DateTime::createFromImmutable($object);
}
if ($object instanceof \DateTime) {
return clone $object;
}
throw new \InvalidArgumentException('Unexpected type');
}
}
else:
trait DateTimeCreateFromInterfacePolyfill {}
endif;
// path/to/DateTimeImmutableCreateFromInterfacePolyfill.php
if (PHP_VERSION_ID < 80000):
trait DateTimeImmutableCreateFromInterfacePolyfill {
public function createFromInterface(\DateTimeInterface $object): \DateTimeImmutable {
if ($object instanceof \DateTime) {
return \DateTimeImmutable::createFromMutable($object);
}
if ($object instanceof \DateTimeImmutable) {
return clone $object;
}
throw new \InvalidArgumentException('Unexpected type');
}
}
else:
trait DateTimeImmutableCreateFromInterfacePolyfill {}
endif;
Union Types
PHP has come a long way with types. We now have scalar types, return types, nullable types, and even property types in PHP 7.4!
PHP 8.0 comes with support for Union Types!
In versions prior to PHP 8.0, you could only declare a single type for properties, parameters, and return types. PHP 7.1 and newer versions have nullable types, which means you can declare the type to be null
with a type declaration similar to ?string
.
From PHP 8.0, you can declare more than one type for arguments, return types, and class properties.
class Example {
private int|float $foo;
public function squareAndAdd(float|int $bar): int|float {
return $bar ** 2 + $foo;
}
}
Similar to types enforced in previous PHP versions, PHP will now make sure the function parameters, return types, and class properties belong to one of the types declared in the definition.
Union types prior to PHP 8.0
Nullable types
PHP already has support for nullable types; You can declare a type as nullable by prefixing a ?
sign to the type. For example, if a certain property can be a string
or a null
, you can declare it as ?string
. With PHP 8.0 Union Types, string|null
will be functionally equivalent to ?string
.
iterable
type
There is also the iterable
pseudo-type that is functionally equivalent to array|Traversable
.
PHPDoc comments
PHPDoc standard had support for Union Types. You would have used a PHPDoc comment to declare the types allowed for a certain code block:
class Example {
/**
* @var int|float
*/
private $foo;
/**
* @param int|float $bar
* @return int|float
*/
public function squareAndAdd(int $bar): int {
return $bar ** 2 + $this->foo;
}
}
Union Types in PHP 8.0
From PHP 8.0, you can now declare any number of arbitrary types to for properties, arguments, and return types. There are a few exceptions that don't make sense to be used together, such as string|void
.
You can completely get rid of the @var
, @param
, and @return
PHPDoc comments now in favor of the types enforced in the code itself. This will certainly help clean the boilerplate comments that were there purely because they could not be declared in code before.
Existing nullable types are not removed, nor deprecated. You can continue to use ?string
as a shorthand to string|null
.
The iterable
type is not going to be removed either, and will be functionally equal to array|Traversable
.
The RFC further explains certain restrictions and other improvements to Union Types:
The void
type is not allowed in a Union
PHP already has support for void
type. It is only allowed as a return type because using void
anywhere else wouldn't make any sense.
Either the function must not return any value, or call return;
without specifying a value.
In PHP 8.0 Union Types, the void
type is not allowed to be combined with any other type.
For example, the following is not allowed:
function foo(): void|null {}
Special false
type
PHP core and many legacy libraries return false
to indicate negative result.
For example, if you have a function that loads use account by its ID, it might return false
to indicate that the user does not exists.
To help the adoption of union types, it is allowed to use false
as part of the union types.
function user_load(int $id): User|false {
}
The snippet above mimics Drupal's user_load()
function, where it returns a User
object if the user account is found, or false
otherwise. This special false
type can be quite helpful in this situation.
There are several PHP core functions that follow the same pattern too. For example, strpos()
function returns an int
of the position where the needle bytes were found in the string, or false
if it is not found anywhere. The special false
type can come handy here too.
false
cannot be used as a standalone type. This means a type declaration likepublic false $foo
is not allowed. You can still usebool
type for it.- There is no
true
pseudo type. The aim of thefalse
pseudo type is to accommodate the (mostly legacy) code that returnsfalse
on failure. Usebool
type there. false
pseudo type is allowed anywhere types are allowed: class properties, function arguments, and return types all supportfalse
type.- *If
bool
is used,false
cannot be used in same type declaration.
Nullable types (?TYPE
) and null
must not be mixed.
While ?string
is a shorthand for string|null
, the two notations must not be mixed.
If your type declaration has more than one type and null, it must be declared as the following:
TYPE_1|TYPE_2|null
It must not be declared as ?TYPE_1|TYPE_2
as this would be an ambiguous declaration.
Compile-time error checking
Union types does not allow duplicate or redundant types in type declaration. This check will happen at compile-time without autoloading classes/interfaces.
Duplicate types are not allowed.
You cannot declare int|int
or int|INT
as this essentially is the same. Furthermore, int|?int
is not allowed either.
The latter will result in a syntax error, and the former will throw this error:
Fatal error: Duplicate type ... is redundant in ... on line ...
Redundant types are not allowed.
Union types does not allow redundant class types. However, it is important to note that this only applies to a few special special types only.
bool|false
is not allowed becausefalse
is a type ofbool
.- `object cannot be used with a class name because all class objects are of type
object
too. iterable
cannot be used witharray
orTraversable
becauseiterable
is a union typearray|Traversable
already.- Class names can be used even if one extends another.
Because the Union types declarations are validated at compile-time, there will be no errors thrown if you use a parent class and a child class in union because that would require class hierarchy resolution.
Here are some examples:
function foo(): bool|false {} // Fatal error: Duplicate type false is redundant in ... on line ...
function foo(): DateTime|object {} // Fatal error: Type DateTime|object contains both object and a class type, which is redundant in ... on line ...
function foo(): iterable|array {} // Fatal error: Type iterable|array contains both iterable and array, which is redundant in ... on line ...
Because the type declaration is only checked at compile-time, the following is valid:
class A{}
class B extends A{}
function foo(): A|B {}
Type variance
Union type variance follows LSP. This is to ensure that all sub classes and implementations of the interface must not change the behavior of the program and still adhere to the contract. This is enforced in PHP even without Union types.
- Parameter are contra-variant: Types can be widened with a super type.
- Return types are covariant: Types can be restricted to a sub type.
- Property types invariant: Types cannot be changed to a sub or super type.
Adding or removing types to a Union
class A{
public function foo(string|int $foo): string|int {}
}
class B extends A{
public function foo(string|int|float $foo): string {}
}
In the snippet above, parameter types are widened (with the addition of float
type). This is allowed because all programs that use class B
expect them to accept all types class A
accepts. Class B
still fulfills this contract.
Return type in B::foo
is restricted further, but this still follows LSP because class B
fulfils the return type declared in A::foo
.
If you were to change the type in a way that class B
does not fulfill the contract, this will trigger a fatal error:
class A{
public function foo(string|int $foo): string|int {}
}
class B extends A{
public function foo(string|float $foo): string|int {}
}
Fatal error: Declaration of B::foo(string|float $foo): string|int must be compatible with A::foo(string|int $foo): string|int in ... on line ...
Variance of individual types in a Union
Same variance rules are followed when you change individual members a union type.
Subclasses
class ParentClass {}
class ChildClass extends ParentClass{}
class FooParent {
public function foo(string|ChildClass $a): string {}
public function bar(string $b): string:ParentClass {}
}
class FooChild extends FooParent{
public function foo(string|ParentClass $a): string|ChildClass {}
public function bar(string $b): string:ChildClass {}
}
This is perfectly, because in the FooChild::foo
functions parameters are expanded. All existing code that works with FooParent
class will work because ChildClass
is a sub-type of ParentClass
with same functionality.
In FooChild::bar
function, its return type is further restricted. This is also valid because FooChild::bar
still fulfills its contract by return a sub-type of string
or ChildClass
which inherits ParentClass
.
Nullable
class FooParent {
public function foo(string $a): int|null {}
}
class FooChild extends FooParent{
public function foo(string|null $b): int|null {}
}
Return types are allowed to lose nullable
type, and parameters are allowed to make they accept null
.
bool
and false
In a Union type, PHP considers false
to be a sub-type for bool
. This allows variance like this:
class FooParent {
public function foo(int|false $a): int|bool {}
}
class FooChild extends FooParent{
public function foo(int|bool $b): int|false {}
}
Backwards compatibility impact
Union Types is a new feature in PHP, and its new syntax does not break any existing functionality. Because this is a syntax change in the engine, this functionality cannot be brought to older versions with a poly-fill.
Default error reporting is set to `E_ALL`
The PHP INI directive error_reporting
controls which types of errors, warnings, and notices should be reported (either to screen or to logs).
In PHP 8.0, the default is changed to E_ALL
.
- error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
+ error_reporting = E_ALL
The error_reporting
value expects a bit-mask, and the default value prior to PHP 8.0 was the value recommended for production: E_ALL & ~E_DEPRECATED & ~E_STRICT
. This value prevented deprecation notice and strict notices from making it to error logs or screen.
In a production system, the
display_errors
value must be set toOff
, which will hide all errors from being printed on the screen, but allows error logs to record them.
Backwards Compatibility Impact
With the default error_reporting
value set to E_ALL
, PHP will trigger the error handler for all PHP errors, notices, and warnings. This is a good move because it can reveal deprecation notices and that were hidden due to the previous configuration.
To bring back the same functionality as PHP 7.4:
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
Related Changes
Inheritance rules are not applied to `private` class methods
PHP class methods declared private
are not accessible by child classes, but child classes can declare a method with the same name without having to adhere to Liskov Substitution Principle. PHP 8.0 strictly enforces method signatures, but private
class methods are not affected by this change.
Typed Properties introduced in PHP 7.4 already follow the same changes, and is not changed in PHP 8.0.
Prior to PHP 8.0, it was not allowed change to change the final
or static
flags of a class method. This is changed in PHP 8.0, that ensures private
methods can be truly private without adhering to the inheritance rules enforced from parent class(es).
In all PHP versions,
private
class method parameters,private
method return types, andprivate
class property types are not validated against Liskov Substitution Principle. These changes in PHP 8.0 only applies to changes inabstract
,static
, andfinal
flags in class methods.
final private
methods
Declaring a class method final private
does not make sense because all private
class methods are inaccessible by child classes anyway.
Prior to PHP 8.0, it was allowed to declare a class method as final private
, but it was not allowed to redeclare the method due to final
clause enforcement.
class Foo {
final private function testFoo(): void {}
}
class ChildFoo extends Foo{
private function testFoo(): void {}
}
This was not allowed prior to PHP 8.0, and resulted in a fatal error:
Fatal error: Cannot override final method Foo::testFoo() in ... on line ...
In PHP 8.0 and later, this restriction is removed, and only a warning is emitted:
Warning: Private methods cannot be final as they are never overridden by other classes in ... on line ...
Note that this warning is emitted when a class declares a method as final private
; not when a child class re-declares a private
method.
final private
class constructors
An exception is made for the warning mentioned above for class constructors. It is possible to declare a class constructor as final private
:
class Foo {
final private function __construct() {}
}
Attempting to re-declare the constructor results in the same fatal error in all PHP versions.
class Foo {
final private function __construct() {}
}
class ChildFoo extends Foo{
final private function __construct() {}
}
// Fatal error: Cannot override final method Foo::__construct() in ... on line ...
static
methods
Changing the static
flag in an inheritance was not allow prior to PHP 8.0. It is allowed in PHP 8.0.
class Foo {
private function bar() {}
static private function baz() {}
}
class ChildFoo extends Foo{
static private function bar() {} // Adds `static` flag from Foo
private function baz() {} // Removes `static` flag from Foo
}
Prior to PHP 8.0, this caused fatal errors:
Fatal error: Cannot make non static method Foo::bar() static in class ChildFoo in ... on line ...
Fatal error: Cannot make static method Foo::baz() non static in class ChildFoo in ... on line ...
abstract
methods
PHP does not allow to declare a private abstract function
in any PHP version. In PHP 8.0 and later, it is allowed to declare a method as abstract
even if a method with same name exists in the parent class.
class Foo {
private function test() {}
}
abstract class ChildFoo extends Foo{
abstract protected function test();
}
Prior to PHP 8.0, this resulted in a fatal error:
Fatal error: Cannot make non abstract method Foo::test() abstract in class ChildFoo in ... on line ...
Magic Methods
In PHP 8.0, [magic method signatures are enforced](). Magic methods (e.g __get
, __set
, __callStatic
, etc) must follow the signature even if they are declared private
.
Backwards Compatibility Impact
All private method signature patterns were not allowed and resulted in a fatal error in PHP versions prior to 8.0. As PHP 8.0 now allows it, these changes should not cause further errors in PHP 8.0
Declaring a class method final private
emits a PHP warning at the declaration time of the parent class. This behavior is different from the fatal error that is only triggered if a child class attempted to extend/re-declare the same method.
Related Changes
- Fatal errors on incompatible method signatures
- Class magic method signatures are strictly enforced
- Calling non-static class methods statically result in a fatal error
Calling non-static class methods statically result in a fatal error
PHP 8.0 no longer allows to call non-static class methods with the static call operator (::
).
Calling non-static methods statically raised a PHP deprecation notice in all PHP 7 versions, and raised a Strict Standards notice in PHP 5 versions.
class Foo {
public function bar() {}
}
Foo::bar();
// Deprecated: Non-static method Foo::bar() should not be called statically in ... on line ...
In PHP 8.0 and later, this results in a fatal error:
class Foo {
public function bar() {}
}
Foo::bar();
// Fatal error: Uncaught Error: Call to undefined method Foo::bar() in ...:...
Note that this only affects calling non-static methods statically. Although discouraged, calling a static method non-statically (
$this->staticMethod()
) is allowed.
This change is implemented throughout the engine.
Variable Functions
class Foo {
public function bar() {}
}
['Foo', 'bar']();
// Fatal error: Uncaught Error: Non-static method Foo::bar() cannot be called statically in ...:...
Callables
PHP no longer considers an array with class name and a method (['Foo', 'bar']
) as a valid callable, and results in a fatal error. This includes PHP core functions that expect a callable. If such callable is passed to a function that expects a valid callable, a \TypeError
will be thrown instead of a fatal error at call-time.
class Foo {
public function bar() {}
}
call_user_func(['Foo', 'bar']);
call_user_func_array(['Foo', 'bar'], []);
// Fatal error: Uncaught TypeError: call_user_func(): Argument #1 ($function) must be a valid callback, non-static method Foo::bar() cannot be called statically in ...:...
This affects all functions ranging from call_user_func
and call_user_func_array
to register_shutdown_function
, set_error_handler
, set_error_handler
.
register_shutdown_function
function in PHP 8.0 versions until beta3 raised a PHP warning at the timeregister_shutdown_function
function is called instead of the current behavior of throwing a\TypeError
exception. This was corrected in PHP beta4.
is_callable
is_callable
function returns false
on callable that calls non-static methods statically. It returned true
prior to PHP 8.0.
class Foo {
public function bar() {}
}
is_callable(['Foo', 'bar']); // false
static
, self
, and parent
static
, self
, and and parent
pointers can continue to use the static call syntax inside a class.
class Test extends UpperTest{
public function foo(): {}
public function bar() {
static::foo();
self::foo();
parent::foo();
}
}
The call above is still allowed because static
, self
, and parent
are used inside the class scope.
static::
and self::
calls are identical to $this->
calls on non-static methods, and improves readability. In the example above, static::foo()
and self::foo()
calls can be safely replaced with $this->foo()
to improve readability because foo
is not a static method.
Backwards Compatibility Impact
For existing code that get fatal errors in PHP 8.0, the fix can as simple as using the correct syntax if there is a class instance in the same scope.
class Foo {
public function bar() {}
}
$foo = new Foo();
- Foo::bar();
+ $foo->bar();
If there is no instantiated class object, and if the class can be instantiated without any parameters or side effects, it will be simple replacement as well.
class Foo {
public function bar() {}
}
- Foo::bar();
+ (new Foo())->bar();
If the class constructor requires parameters, or tends to make any state changes, the fix can be more complicated. The instance needs to be injected to the scope the static call is made.
Note that functions that expect a callable
parameter no longer accept callables with a non-static method as static method. A \TypeError
exception will be thrown when the callable is passed, as opposed to when the callable is invoked.
class Foo {
public function bar() {}
}
function takeCallable(callable $func) {}
- takeCallable(['Foo', 'bar']);
+ takeCallable([new Foo(), 'bar']);
is_callable
function no longer returns true
for such callables either.
Apache Handler: Module name and file path changes
The Apache 2 Handler SAPI in PHP 8.0 is renamed, and can affect when PHP is used as an Apache module.
It is recommended to use PHP-FPM with Apache mod_event
MPM. However, PHP 8.0 still supports the Apache 2 Handler SAPI, which is integrated with Apache 2 web server as an Apache module.
Module identifier renamed to php_module
In PHP 8.0, the Apache module identifier is renamed to php_module
.
In PHP 7.x versions, it was named php7_module
, and in PHP 5.x, it was named php5_module
.
This means that all references to the Apache module loading configuration needs to be updated:
- LoadModule php7_module "/usr/lib/apache2/modules/libphp7.4.so"
+ LoadModule php_module "/usr/lib/apache2/modules/libphp8.0.so"
Apache will throw an error similar to the one below if the module name php8_module
is used:
Can't locate API module structure `php8_module` in file ...
Apache <IfModule>
directives, and any other places the module name is used must be updated to their new name as well:
- <IfModule php7_module>
+ <IfModule php_module>
DirectoryIndex index.html default.php index.php
AddHandler application/x-httpd-php .php
</IfModule>
Windows DLL file name changes
On Windows, the DLL file name is updated for PHP 8. Module identifier is php_module
.
- LoadModule php7_module "W:/php/php-8.0.0-Win32-vc15-x64/php7apache2_4.dll"
+ LoadModule php_module "W:/php/php-8.0.0-Win32-vc15-x64/php8apache2_4.dll"
It might be necessary to manually update development environment bundles such as XAMPP, Wamp, or Laragon to reflect this.
Backwards Compatibility Impact
Due to the module identifier renaming, errors similar to Can't locate API module structure php8_module in file
might occur, but most distributions, software collections, and PPAs automatically configure the Apache2Handler on behalf of the user, so the chances of end users running into this issue is uncommon.
Locale-independent `float` to `string` casting
PHP 8.0 changes the way PHP coerces float
values to string
.
Prior to PHP 8.0, float
to string
conversions depended on the current locale set with the setlocale
function. setlocale
function is not thread-safe, which means setting locale will affect all threads in a PHP process.
setlocale(LC_ALL, "fr_FR");
echo 1.618; // 1,618
// ^^^ comma
PHP does not inherit the system locale by default, but calling setlocale
without a specific locale makes it inherit the system locale. If setlocale()
is called without an explicit locale set (e.g. setlocale(LC_ALL, null)
, PHP will take the system locale from LC_ALL
environment variable. System locale can be obtained by running locale
in terminal.
Both comma (,
) and period (.
) symbols are accepted as a valid decimal separator throughout the world, and there is no international standard that prefers one over another. However, PHP can only accept the period notation for floats:
$val = 1,618;
// PHP Parse error: syntax error, unexpected ',' in ... on line ...
PDO
extension, and functions such as var_export
, json_encode
and serialize
try to minimize the locale-dependence with special handling for float values.
In PHP 8, all string
to float
conversions will be locale-independent. This means even if PHP is running under a different locale that prefers periods for the decimal separator, PHP will not use that locale when float values are presented.
setlocale(LC_ALL, "de_DE");
echo 1.618; // 1.618
// ^^^ period
printf
functions and specifiers
printf
-line of functions provide several modifiers to format and convert data formats.
*printf
specifiers will continue to use the %f
specifier in a locale-dependent way. It is already noted in the documentation that %f
output is locale-aware.
For locale-independent float formatting, use %F
specifier.
setlocale(LC_ALL, "fr_FR");
printf("%f", 1.618); // 1,618000
printf("%.3f", 1.618); // 1,618
printf("%F", 1.618); // 1.618000
printf("%.3F", 1.618); // 1.618
In PHP 8, using the string specifier (%s
) will have the locale-independent float to string effect.
setlocale(LC_ALL, "fr_FR");
// PHP < 8.0
printf("%s", 1.618); // 1,618
// PHP >= 8.0
printf("%s", 1.618); // 1.618
Further, %g
and %G
specifiers are locale-aware, and continue to be so in PHP >= 8.0 as well. There are new %h
and %H
specifiers in PHP 8.0, that provide the same functionality as %g
and %G
specifiers, but in a locale-independent way.
setlocale(LC_ALL, "fr_FR");
// All PHP versions
printf("%g", 1.618); // 1,618
printf("%G", 1.618); // 1,618
// New in PHP 8.0
printf("%h", 1.618); // 1.618
printf("%H", 1.618); // 1.618
PHP < 8.0 | PHP >= 8.0 | |
---|---|---|
%s (string) |
Locale-aware | Locale-independent |
%f (float) |
Locale-aware | Locale-aware |
%F (float) |
Locale-independent | Locale-independent |
%g (general) |
Locale-aware | Locale-aware |
%G (general) |
Locale-aware | Locale-aware |
%h (general) |
- | New, Locale-independent |
%H (general) |
- | New, Locale-independent |
Locale-aware float to string coercion
To make a locale-aware float
to string
coercion, it is still possible to use the sprintf
function with %f
or %g
specifiers. These specifiers are in locale-aware in all PHP versions.
setlocale(LC_ALL, "fr_FR");
- echo (string) 1.618; // 1.618
+ echo sprintf("%g", 1.618); // 1,618
Note that setlocale
function can be the root of side-effects due to the way it works; the locale is set per-process, and not per-thread, which opens up the possibility of one script/request setting the locale, and having that effect in other threads in the same process.
An ideal approach would be to use the intl
extension and its format helpers which are arguably much more flexible, easy to test, and use with different locales simultaneously.
$formatter = new \NumberFormatter("fr_FR", \NumberFormatter::DECIMAL);
echo $formatter->format(1.618); // 1,618
intl
NumberFormatter
performance
100K iterations of theNumberFormatter::format
method took0.012 sec
, compared to0.004 sec
forsprintf
. While it is a 3x performance difference, it is an absolutely minimal performance impact that wouldn't even register.
Backwards Compatibility Impact
This change can cause backwards-compatibility issues, which can be difficult to spot at once.
When strict types are not enabled, PHP silently converts float and string types back and forth when cast, or when used in parameter, property, or return types. Applications with strict types enabled would have already raised on errors when type silent coercion occur; For the rest this change will silently treat float-strings as locale-independent.
If you use setlocale
function to set a locale, and relied on this PHP behavior to automatically format float values, it will now be necessary to explicitly format them to a locale-aware format.
Related Changes
- PHP 7.4: Underscore numeric separator
- Union Types
- New
Stringable
interface - New
mixed
pseudo type - New
%h
and%H
printf
specifiers - New
*
precision and width modifiers inprintf
Class magic method signatures are strictly enforced
Magic methods in PHP are special class method names that, if declared, brings special functionality to the class.
There are several magic methods in PHP. __constuct()
magic method is called when a new class object is instantiated with new Foo()
pattern, the __get()
magic method is called when non-existing class property is requested, __toString()
magic method is called when a class object is coerced to a string
, and so on.
Although all these magic methods carried a semantic meaning, they were not programmatically enforced until PHP 8.0. The __toString()
method, for example, semantically expects to return a string
, but it was possible to return any data type, including ones that cannot be coerced to strings.
class Foo {
public function __toString(): object {
}
}
In PHP 8.0 and later, if a magic method is declared, it must adhere to the function signature and return type if declared too.
A few signature checks are enforced for return types since their introduction in PHP 7.0. Namely, __construct
, __destruct
, and __clone
are were not allowed to declare a return type at all, not even void
(void
was added in PHP 7.1).
Types are optional
Unlike a PHP class interface
, that enforces the parameter and return types to be present in all implementations, magic method enforcement allows to not declare a type at all.
This allows existing classes that implement magic methods to continue to run without having to declare explicit types.
However, if a type is declared, it must follow the signature.
Fatal Errors on Signature Mismatches
PHP 8.0 throws fatal errors when interface implementations and class inheritance violates LSP. Magic methods follow the same pattern.
- Return types are allowed to narrow the return type.
- Method parameters are allowed to widen the parameter type.
Variance is allowed
Similar to standard class/interface inheritance, magic methods variance is allowed too.
For example, a __get()
method which should return a mixed
type can declare that its return type is bool
because bool
is already included in the mixed
type.
Further, the __set()
magic method can widen its method parameter from __set(string $name, $value)
to __set(string|CacheEntry $name, $value)
if the function can handle CacheEntry
objects too.
Magic Method Signatures
class GoodFoo {
public function __isset(string $name): bool {}
public function __get(string $name): mixed {}
public function __set(string $name, mixed $value): void {}
public function __unset(string $name): void {}
public function __set_state(array $properties): object {}
public function __call(string $name, array $arguments): mixed {}
public function __callStatic(string $name, array $arguments): mixed {}
public function __invoke(mixed $arguments): mixed {}
public function __clone(): void {}
public function __serialize(): array {}
public function __unserialize(array $data): void {}
public function __sleep(): array {}
public function __wakeup(): void {}
public function __debugInfo(): ?array {}
}
Any magic method implementation must take all parameters in their signature, and if declares a return type, it must be of the same type of a sub type too.
Any declarations otherwise triggers a fatal error:
class BadFoo {
public function __isset(string $name): array {}
// mismatch ^^^^^
}
// Fatal error: BadFoo::__isset(): Return type must be bool when declared in ... on line ...
class BadBar {
public function __call(array $name, array $arguments): mixed {}
// mismatch ^^^^^
}
// Fatal error: BadBar::__call(): Parameter #1 ($name) must be of type string when declared in /in/JpKPd on line 4
Magic methods with no return types allowed
Magic methods that did not allow declaring any return type prior to PHP 8.0 continue to enforce this.
class BadBar {
public function __construct(): void {}
public function __destruct(): void {}
}
Neither of the declarations are allowed in PHP 8.0 and prior versions. They result in a fatal error:
Fatal error: Method BadBar::__construct() cannot declare a return type in ... on line ...
Fatal error: Method BadBar::__destruct() cannot declare a return type in ... on line ...
Stringable
interface
PHP 8.0 introduces a new Stringable
that is added automatically to any class that implements __toString
magic methods. If the Stringable
interface is explicitly declared (e.g Foo implements Stringable
), the __toString()
method signature is enforced with interface rules.
private
Magic Methods
PHP 8.0 relaxes signature enforcement when a private
method is re-declared in a child class. Magic methods must follow the signature even if they are declared private
.
class Foo {
private function __get(): mixed {}
}
// Fatal error: Method Foo::__get() must take exactly 1 argument in ... on line ...
Backwards Compatibility Impact
This is backwards-compatibility breaking change. However, the enforced signatures are semantically correct, and allows variance, mitigates potential compatibility issues.
- If there are no types enforced, the their types will not be checked. However, the methods must accept same number of parameters.
mixed
type is includes all types in PHP exceptvoid
, which makes any return type fulfills return type signature.
Related Changes
- Fatal errors on incompatible method signatures
- Calling non-static class methods statically result in a fatal error
- Inheritance rules are not applied to
private
class methods
`substr`, `iconv_substr`, `grapheme_substr` return empty string on out-of-bound offsets
substr
, mb_substr
, iconv_substr
, and graphme_substr
functions in PHP provides a way to retrieve a part of the provided string.
Prior to PHP 8, if the offset parameter is longer than the provided string itself, it returned a boolean false
. This violated the documented function signature that mentioned string
as the return type.
For example, prior to PHP 8, the following snippet returns false
on these functions:
substr('FooBar', 42, 3); // false
mb_substr('FooBar', 42, 3); // ""
iconv_substr('FooBar', 42, 3); // false
grapheme_substr('FooBar', 42, 3); // false
In the snippet above, the string offset parameter is 42, although the string itself is only 6 characters. substr
and its complement functions (except mb_string
) from other extensions return false
.
In PHP 8, the behavior of these functions have changed.
substr
substr
returns an empty string if the offset is larger than the length of the string. Prior to PHP 8, it returned false
.
substr('FooBar', 42); // ""
mb_substr
mb_substr
already returns an empty string in all PHP versions, and is not changed in PHP 8.0.
iconv_substr
iconv_substr
returned false
if the offset is larger than the length of the provided string. This is changed in PHP 8 to return an empty string.
iconv_substr('FooBar', 42); // ""
On negative string offsets, iconv_substr
function clamps the offset to the length of the string.
iconv_substr('FooBar', -42, 4); // "FooB"
grapheme_substr
grapheme_substr
returns an empty string if the offset is larger than the length of the string. Prior to PHP 8, it returned false
similar to substr
.
grapheme_substr('FooBar', 42); // ""
This now applies to all the following functions:
substr
mb_substr
frommbstring
extensioniconv_substr
fromiconv
extensiongrapheme_substr
fromintl
extension
substr('FooBar', 42, 3); // ""
mb_substr('FooBar', 42, 3); // ""
iconv_substr('FooBar', 42, 3); // ""
grapheme_substr('FooBar', 42, 3); // ""
The offset parameter can be a negative string, which makes substr
return a portion of the string counted from the end of the provided string. If the negative string offset exceeds the length of the string, the cursor will not go beyond that. This functionality was not changed for substr
and mb_substr
functions.
iconv_substr
and grapheme_substr
functions now clamp negative offsets to the length of the string, following substr
and iconv_substr
functions.
PHP < 8.0 | PHP >= 8.0 | |
---|---|---|
substr('FooBar', 42) |
false |
"" |
substr('FooBar', -42, 4) |
"FooB" |
"FooB" |
mb_substr('FooBar', 42) |
"" |
"" |
mb_substr('FooBar', -42, 4) |
"FooB" |
"FooB" |
iconv_substr('FooBar', 42) |
false |
"" |
iconv_substr('FooBar', -42, 4) |
false |
"FooB" |
grapheme_substr('FooBar', 42) |
false |
"" |
grapheme_substr('FooBar', -42, 4) |
false |
"FooB" |
Backwards Compatibility Impact
The return type of substr
and iconv_substr
functions will now be string
, from the previous string|false
, which brings them in line with mb_substr
function.
If you relied on these functions return value to be false
, it will now return an empty string (""
) instead, which is arguably the more semantically correct behavior.
This change was made after PHP 8.0 beta 4 (final beta) was released. It will only be effective in PHP 8.0 versions RC1 and later, including GA (general availability) versions. In PHP 8.0 versions prior to RC1,
grapheme_substr
function was set to throwValueError
exceptions on invalid offsets.
Related Changes
- New
Stringable
interface - New
str_contains
function - New
str_starts_with
andstr_ends_with
functions - String functions consider an empty string occur at every position in a string
PHP Startup Errors are displayed by default
PHP Startup Errors are emitted when PHP parses the INI files and starts up. These errors are different from standard application errors and their visibility is toggled with display_startup_errors
INI directive. .
INI setting display_startup_errors
controls whether the startup errors should be displayed.
Prior to PHP 8.0, the INI directive was set to Off
, potentially hiding useful information. This value is set to On
by default in PHP 8.0.
- display_startup_errors=Off
+ display_startup_errors=On
session.name = ""
The INI setting above is invalid, because the session.name
must be a non-empty non-numeric string. PHP emits a startup warning if it encounters such value:
# php -d session.name="" test.php
Warning: PHP Startup: session.name cannot be a numeric or empty '' in Unknown on line 0
A few such potential startup errors include:
- Missing the module file (
extension=foo.so
):Warning: PHP Startup: Unable to load dynamic library 'foo.so'
- Invalid
session.name
value (session.name=""
):Warning: PHP Startup: session.name cannot be a numeric or empty '' in Unknown on line 0
- Invalid
session.upload_progress.freq
value (session.upload_progress.freq=101%
):Warning: PHP Startup: session.upload_progress.freq must be less than or equal to 100% in Unknown on line 0
Backwards Compatibility Impact
This is a mere default INI value change. It is possible to set this value back to Off
in an INI file (more effective) or with a ini_set
call (less effective).
display_startup_errors=Off
ini_set('display_startup_errors', 'Off');
Related Changes
GD Extension: Windows DLL file name changed from `php_gd2.dll` to `php_gd.dll`
GD Extension: Windows DLL file name changed from php_gd2.dll
to php_gd.dll
If you were to copy php.ini
a PHP 7 php.ini
file to PHP 8, it now needs to use the GD extension with php_gd.dll
, or in its extension-less format:
- extension=gd2
+ extension=gd
In Linux systems such as Ubuntu/Debian, the GD extension is loaded with name gd.so
throughout all PHP versions, and no changes are necessary.
Further, GD extension now uses GdImage
class objects instead of resources as its primary data object.
Backwards Compatibility Impact
Using gd2
as the extension name to load will result in a PHP startup warning (which are shown by default in PHP 8) because the DLL file is now renamed in to php_gd.dll
:
Warning: PHP Startup: Unable to load dynamic library 'gd2' (tried: .../ext\gd2 (The specified module could not be found.), .../ext\php_gd2.dll (The specified module could not be found.)) in Unknown on line 0
`crypt()` function requires `$salt` parameter
PHP crypt()
function requires its $salt
parameter to be passed in PHP 8.0 and forward, changing from its prior behavior of raising a notice when the parameter was not passed.
crypt()
function is largely replaced by password_hash()
function and provides secure defaults. Not passing a password salt to the crypt()
function makes PHP generate one, but due to historical reasons, this salt is insecure by today's security standards. Further, the salt had to adhere to certain patterns to switch certain hashing mechanisms.
Unless your application requires compatibility with other APIs that need to store crypt()-compatible hashes, consider using
password_hash
/verify
/needs_rehash()
functions.
Using crypt()
function without a specific $salt
parameter makes PHP pick a hashing algorithm based on the availability of the mechanisms in the system it runs on, and thus can lead to unexpected (and often insecure) results.
$hash = crypt('p4ssw0rd');
The above snippet results in a PHP notice since PHP >=5.6 and PHP < 8.0:
Notice: crypt(): No salt parameter was specified. You must use a randomly generated salt and a strong hash function to produce a secure hash. in ... on line ...
Since PHP 8.0, the second parameter is required, and thus will result in an ArgumentCountError
exception.
ArgumentCountError: crypt() expects exactly 2 parameters, 1 given
Transition to PHP 8
It might be possible to detect the salt PHP automatically generated prior to PHP 8.0, and provide the very same to the crypt()
function, so you can verify the password before moving to password_hash()
function.
When the $salt
parameter is not provided to the crypt()
function, a random salt is generated, and returned along the the returned hash.
crypt('p4ssw0rd');
// $1$N1EHVYY9$qz.BE7oyWWkD85o5uCvO8/
In the example above, $1$N1EHVYY9$
(different in each invocation) is the random salt, and $1$
indicates that the hash is of CRYPT_MD5
type. Various mechanisms such as CRYPT_SHA512
orCRYPT_BLOWFISH
will contain different identifiers, but they all will follow the same pattern, that can be captured with the Regular Expression below:
(^\$.+\$).+
Until your application is using password_hash()
, you will be able to temporarily authenticate users
$pattern = '/(^\$.+\$).+/';
$stored_hash = '$1$N1EHVYY9$qz.BE7oyWWkD85o5uCvO8/';
preg_match($pattern, $stored_hash, $matches);
if (empty($matches[1])) {
return false;// Unknown salt pattern.
}
$hash = crypt('p4ssw0rd', $matches[1]);
if (!hash_equals($hash, $stored_hash)) {
return false; // Invalid password.
}
$stored_hash = password_hash('p4ssw0rd', PASSWORD_DEFAULT);
// Now, store $stored_hash back.
...
Backwards compatibility impact
What was previously a PHP notice
due to missing second parameter to crypt()
will result in an ArgumentCountError
exception in PHP 8.0.
You can continue to use the crypt()
function until you migrate to use password_*
functions, as long as the $salt
parameter is passed, or extracted from the stored hash as mentioned in the example above.
password_*
functions are not affected by this change.
PDO: Default error mode set to exceptions
PHP's PDO
extension provides a consistent interface to access databases. it supports various database software, including MySQL, PostgreSQL and SQLite.
For each connection PDO makes to a database, it supports an attribute to specify how PDO should behave when an error occurs with the query, connection, or the database.
PDO::ATTR_ERRMODE
attribute controls how PDO should behave on errors.
PDO::ERRMODE_SILENT
(default): PDO will not raise warnings or throw exceptions in case of an error. It is up to the caller to inspect PDO::errorCode() and PDO::errorInfo() methods on both statement and PDO objects to determine errors.PDO::ERRMODE_WARNING
: Emits a warning (E_WARNING
) in addition to setting the error code and information.PDO::ERRMODE_EXCEPTION
: Throws aPDOException
in case of an error.
The error mode attribute can be set when instantiating the PDO object, or after the connection is established.
$pdo = new PDO($dsn, 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$pdo = new PDO($dsn, 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Prior to PHP 8.0, the default error mode was to be silent (PDO::ERRMODE_SILENT
); PDO did not emit a warning or throw an exception if the database returned an error. Most libraries that deals with database often explicitly set the error mode to PDO::ERRMODE_EXCEPTION
because throwing an exception is desired as it minimizes the chances of overlooking database errors.
$pdo = new PDO('mysql:host=loc...', 'root', '');
$pdo->exec("DELETE FROM probably_missing_table WHERE id=42");
The snippet above, for example, tries to delete a record from a table that does not exist. It is desired that that the database error SQLSTATE[42S02]: Base table or view not found: 1146 Table 'phpwatch.myguests' doesn't exist
is brought as a PDOException
.
In PHP 8.0, the default PDO::ATTR_ERRMODE
attribute is set to PDO::ERRMODE_EXCEPTION
.
It is still allowed to explicitly set the attribute to PDO::ERRMODE_EXCEPTION
. Applications that already used the PDO::ERRMODE_EXCEPTION
error mode will not need to make any changes.
Backwards Compatibility Impact
Although not recommended, applications that relied on the PDO's silent behavior on errors can set PDO::ATTR_ERRMODE
attribute to PDO::ERRMODE_SILENT
. This will make PDO silently ignore all errors throughout the lifespan of that database connection.
$pdo = new PDO($dsn, 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT
]);
To selectively ignore database errors, it is more appropriate to catch that error instead of setting the PDO::ATTR_ERRMODE
attribute throughout the connection.
- $pdo->exec("DELETE FROM probably_missing_table WHERE id=42");
+ try {
+ $pdo->exec("DELETE FROM probably_missing_table WHERE id=42");
+ }
+ catch (PDOException) {}
This snippet uses the PHP 8.0 feature to catch exceptions only by type.
Both approaches above are not ideal, as they ignore errors returned from the database, and ideally should be corrected in the query or the database structure.
`@` Error Suppression operator does not silent fatal errors
PHP supports the @
error control operator (also called STFU operator with mixed feelings), that suppresses errors just for the expression that immediately follows.
For example, the unlink
function emits a warning if the file does not exist, and calling it with the @
operator can suppress these errors.
@unlink('file-that-does-not-exist.oops');
This operator was historically used frequently to access array values (such as HTTP parameters), call certain functions that the outcome does not alter the control flow. While PHP is getting strict with better exception handling, and type checking for internal functions since PHP 8.0, the @
suppression operator is still useful in various areas, such as file system operations.
Related Articles
In PHP 8.0, the @
operator does not suppress certain types of errors that were silenced prior to PHP 8.0. This includes the following types of errors:
E_ERROR
- Fatal run-time errors.E_CORE_ERROR
- Fatal errors occurred in PHP's initial startup.E_COMPILE_ERROR
- Fatal compile-time errors (from Zend engine).E_USER_ERROR
- User-triggered errors withtrigger_error()
function.E_RECOVERABLE_ERROR
- Catchable fatal error.E_PARSE
- Compile-time parse errors.
All of these errors, if raised, halts the rest of the application from being run. The difference in PHP 8.0 is that the error message is not silenced, which would have otherwise resulted in a silent error.
Related Changes in PHP 8.0
Note that the @
operator continue to silent warnings and notices.
Here is an example that this change in PHP 8.0 makes a difference.
function load_files() {
require_once 'file-that-does-not-exist.oops';
}
@load_files();
Prior to PHP 8.0, this snippet would not have emitted any errors or notices (but stopped execution anyway). In PHP 8.0, this error (E_ERROR
) is not suppressed, and the execution is stopped too.
Fatal error: Uncaught Error: Failed opening required 'file-that-does-not-exist.oops' (include_path='.:') in ...:...
Another example where this change is more pronounced is with the E_USER_ERROR
error type.
function do_something() {
trigger_error('Something went wrong', E_USER_ERROR);
}
@do_something();
While PHP versions prior to 8.0 suppresses this E_USER_ERROR
, PHP 8.0 and later do not, and result in an error:
Fatal error: Something went wrong in ... on line ...
Error Type | Error number | @ effectivePHP < 8.0 |
@ effectivePHP >= 8.0 |
---|---|---|---|
E_ERROR |
1 | Yes | No |
E_WARNING |
2 | Yes | Yes |
E_PARSE |
4 | Yes | No |
E_NOTICE |
8 | Yes | Yes |
E_CORE_ERROR |
16 | Yes | No |
E_CORE_WARNING |
32 | Yes | Yes |
E_COMPILE_ERROR |
64 | Yes | No |
E_COMPILE_WARNING |
128 | Yes | Yes |
E_USER_ERROR |
256 | Yes | No |
E_USER_WARNING |
512 | Yes | Yes |
E_USER_NOTICE |
1024 | Yes | Yes |
E_STRICT |
2048 | Yes | Yes |
E_RECOVERABLE_ERROR |
4096 | Yes | No |
E_DEPRECATED |
8192 | Yes | Yes |
E_USER_DEPRECATED |
16384 | Yes | Yes |
E_ALL |
32767 | Yes | No |
The
@
operator does not suppress any exceptions in any PHP version. Several PHP versions now throw\ValueError
and\TypeError
exceptions. See PHP 8.0: Internal function warnings now throwTypeError
andValueError
exceptions.
Error handlers set with set_error_handler
function will continue to receive errors they subscribe to.
Backwards Compatibility Impact
With this change in PHP 8.0, the @
operator might not suppress all the errors. This is a desired result, because all the error messages that are not silenced also cause the application to halt. Suppressing said errors would not allow the application to continue.
For recoverable errors that emit an E_USER_ERROR
, it might be a good alternative to throw an exception instead, which makes it possible for the caller to catch it if possible.
If the pre-PHP 8.0 behavior is required:
- Adjust the custom error handler to ignore the specific types of error. Error handlers receive the error number as its first argument.
- Temporarily lower the
error_reporting
value to0
, before calling the potentially erroneous expression.
An example of a "safe" caller (shall be named "double shrug 🤷🏼♀️🤷🏼♂️ operator", but can take any name):
function 🤷🏼♀️🤷🏼♂️(callable $callable): mixed {
$current_state = error_reporting();
error_reporting(0);
$return = $callable();
error_reporting($current_state);
return $return;
}
function load_files() {
require_once 'file-that-does-not-exist.oops';
}
- @load_files();
+ 🤷🏼♀️🤷🏼♂️(function() {
@load_files()
});
To reiterate, both of these approaches will stop the execution of the application; only the error will be hidden.
Trailing commas are allowed in parameter lists and closure `use` lists
PHP 8.0 syntax allows to leave a trailing-comma in parameter lists and closure use
lists.
function foo(string $foo, string $bar,) {}
function() use ($foo, $bar,) {
}
PHP 7.2 eased the syntax to allow trailing commas in grouped use
statements for grouped use
statements; PHP 7.3 eased the syntax to allow trailing commas in function and method calls, and this change in PHP 8.0 allows trialing commas in function/method declarations and use
lists in closures.
class Request {
public function __construct(
$method,
UriInterface $uri,
HeadersInterface $headers,
array $cookies,
array $serverParams,
StreamInterface $body,
array $uploadedFiles = [],
) {
// ...
}
}
Prior to PHP 8.0, this would have caused a parse error due to the disallowed trailing comma in the parameter list, at array $uploadedFiles = [],
:
Parse error: syntax error, unexpected ')', expecting variable (T_VARIABLE) in ... on line ...
Similarly, trailing commas are allowed in PHP 8.0, but resulted in a parse error in earlier versions:
function() use (
$foo,
$bar,
) {
}
Parse error: syntax error, unexpected ')', expecting '&' or variable (T_VARIABLE) in ... on line ...
One of the major benefits of this is that diff outputs will be cleaner when new parameters or use
variables are appended.
In the snippets above, adding a new parameter or a use
variable to the end would produce simpler diff outputs, because the line with last parameter is not modified.
StreamInterface $body,
array $uploadedFiles = [],
+ array $extraOptions = [],
)
Without the trailing-commas allowed, the diff output would include the changes in the last line as well:
StreamInterface $body,
- array $uploadedFiles = []
+ array $uploadedFiles = [],
+ array $extraOptions = []
)
Backwards Compatibility Impact
Due to this being a syntax change, this feature cannot be back-ported.
All code that contains trailing commas in parameter lists, or closure use
lists will produce parse errors in versions prior to PHP 8.0:
Using trailing commas in parameter lists in PHP < 8.0
Parse error: syntax error, unexpected ')', expecting variable (T_VARIABLE) in ... on line ...
Using trailing commas in closure use
lists in PHP < 8.0
Parse error: syntax error, unexpected ')', expecting '&' or variable (T_VARIABLE) in ... on line ...
Parameter lists
Closure use
lists
RFC Implementation
Implicit negative array key increments do not skip negative numbers
PHP 8.0 changes how it increments negative numeric array keys.
Prior to PHP 8.0, if an array has its last key as a negative number (such as -42
), an array increment operation skipped all negative numbers and selected the next key as 0
or the next positive number. In PHP 8.0, this behavior is dropped, and array keys will be incremented regardless of the sign of the last key number.
$array = [
-42 => 'Foo',
];
$array[] = 'Bar';
Prior to PHP 8.0, the array is incremented to key 0
, because the last key is a negative number.
array(2) {
[-42]=> string(3) "Foo"
[0]=> string(3) "Bar"
}
In PHP 8.0 and later, this special negative array key treatment is removed, and array keys are incremented by adding 1, regardless of the sign.
array(2) {
[-42]=> string(3) "Foo"
[-41]=> string(3) "Bar"
}
This change was planned since PHP 7.3, and was expected to change in PHP 8.0. However, PHP did not emit any deprecation notices or warnings when it encounters such array increments.
All array increment operations are affected. This includes the array_fill
and array_push
functions.
array_fill
In PHP 8.0 and later, array_fill
function behaves similar to the standard array incremental pattern; that it does not skip negative numbers.
$array = array_fill (-42, 3 , 'Value');
var_dump($array);
PHP 8.0 and later:
array(3) {
[-42]=> string(5) "Value"
[-41]=> string(5) "Value"
[-40]=> string(5) "Value"
}
Prior to PHP 8.0, array_fill
function skipped all negative numbers and started from 0
:
array(3) {
[-42]=> string(5) "Value"
[0]=> string(5) "Value"
[1]=> string(5) "Value"
}
array_push
Similar to array_fill
, array_push
function also determines the next array key without skipping negative numbers.
$array = [
-42 => 'Foo',
];
array_push($array, "Bar");
var_dump($array);
PHP 8.0 and later:
array(2) {
[-42]=> string(3) "Foo"
[-41]=> string(3) "Bar"
}
Prior to PHP 8.0, the next key as determined by array_push
skipped negative numbers to 0
or next positive integer:
array(2) {
[-42]=> string(3) "Foo"
[0]=> string(3) "Bar"
}
Backwards Compatibility Impact
This change breaks backwards-compatibility, and might result in subtle bugs that are not easily detected. However, this change will likely be a harmless change because all array values will be stored regardless of the key value.
Note that in all versions, PHP automatically coerces numeric strings (such as "42"
) to an int
(such as 42
) when used as an array key. This happens even if strict types is enabled (declare(strict_types=1)
). It is possible that arrays are initiated with negative numbers without realizing it, and with PHP 8.0, direct access to said arrays (such echo $array[0]
) can result in unexpected behavior.
Code that relied on PHP's behavior of skipping to 0
will not work as intended in PHP 8.0.
`XMLWriter` objects replace `xmlwriter` resources
As part of PHP's resource to object migration, XMLWriter extensions primitive resource
objects are now converted to accept and return the existing XMLWriter
objects.
XMLWriter
extension provides both an object-oriented interface and a procedural function interface.
-
The
XMLWriter
class contains all methods to work with the functionality provided by the extension. -
XMLWriter extensions procedural API, however, used
xmlwriter
resources and thexmlwriter
resource
must be passed as the first parameter to each function.
$writer = new XMLWriter();
$writer->openMemory();
$writer->startDocument("1.0");`
The same functionality in procedural code:
$writer_resource = xmlwriter_open_memory();
xmlwriter_start_document($writer_resource, '1.0');`
In PHP 8.0, the procedural-style functions accept and return XMLWriter
instead of resource
objects.
Object-oriented interface of
XMLWriter
extension has not changed in PHP 8.0. Furthermore, the procedural API functions are not deprecated.
Because the procedural-style functions accept XMLWriter
objects, it is possible to mix the object-oriented and procedural patterns. Note that this can make the code lose its consistency at no benefit. However, this can ease the migration to object-oriented API as the previous resource
objects are inter-changeable with the XMLWriter
objects
is_resource
Note that return values from the procedural API of XMLWriter extension will be standard PHP class objects, and will no longer return true
for is_resource
function calls.
It might be necessary to update existing code to accept XMLWriter
objects as well.
- is_resource($xw)
+ is_resource($xw) || $xw instanceof XMLWriter
However, this is an anti-pattern, because failing to create an xmlwriter
resource results in a false
return value in all PHP versions. A check against false
might be more appropriate:
- is_resource($xw) || $xw instanceof XMLWriter
+ !empty($xw)
Backwards Compatibility Impact
Unless there are is_resource
checks, this resource to object migration is completely opaque.
The object-oriented API continue to work, and the procedural API is not deprecated either. It not handles XMLWriter
objects that can be called interchangeably, as opposed to procedural-specific xmlwriter
resources.
OpenSSL: `resource` to object migration
Along with PHP's resource to object transformation, PHP's OpenSSL extension changes its resource
objects to standard PHP class objects in PHP 8.0.
The newly added class objects are not allowed to be instantiated with new OpenSSL...()
constructs, and must be instantiated with the same functions that returned corresponding resource
objects in prior PHP versions. Further, they are declared as final
, which prevents them from being extended in a sub class.
OpenSSL key
resources toOpenSSLAsymmetricKey
objectsOpenSSL X.509
resources toOpenSSLCertificate
objectsOpenSSL X.509 CSR
resources toOpenSSLCertificateSigningRequest
objects
OpenSSL key
resources to OpenSSLAsymmetricKey
objects
OpenSSL key
resource type (produced by openssl_pkey_new
function) is changed to OpenSSLAsymmetricKey
class objects.
In PHP 8.0, openssl_pkey_new
function returns an instance of OpenSSLAsymmetricKey
class instead of OpenSSL key
resources. All functions that previously accepted resources now accept the class objects as well.
OpenSSLAsymmetricKey
class synopsis
final class OpenSSLAsymmetricKey {}
Instantiating a new object with new OpenSSLAsymmetricKey()
construct will raise an error:
new OpenSSLAsymmetricKey();
PHP Error: Cannot directly construct OpenSSLAsymmetricKey, use openssl_pkey_new() instead in ... on line ...
OpenSSL X.509
resources to OpenSSLCertificate
objects
In PHP 8.0 and later, openssl_x509_read
function returns OpenSSLCertificate
class objects instead of OpenSSL X.509
resources it did in PHP versions prior to PHP 8.0.
All functions that accepted/returned OpenSSL X.509
resources now accept/return OpenSSLCertificate
objects instead.
OpenSSLCertificate
class synopsis
final class OpenSSLCertificate {}
Instantiating a new object with new OpenSSLCertificate()
construct will raise an error:
new OpenSSLCertificate();
PHP Error: Cannot directly construct OpenSSLCertificate, use openssl_x509_read() instead in ... on line ...
OpenSSL X.509 CSR
resources to OpenSSLCertificateSigningRequest
objects
openssl_csr_new
returns OpenSSLCertificateSigningRequest
objects in PHP 8.0 and later. This function returned OpenSSL X.509 CSR
resources in prior PHP versions.
All functions that accepted OpenSSL X.509 CSR
resource
parameters now accept OpenSSLCertificateSigningRequest
objects instead.
OpenSSLCertificateSigningRequest
class synopsis
final class OpenSSLCertificateSigningRequest {}
Instantiating a new object with new OpenSSLCertificateSigningRequest()
construct will raise an error:
new OpenSSLCertificateSigningRequest();
PHP Error: Cannot directly construct OpenSSLCertificateSigningRequest, use openssl_csr_new() instead in in ... on line ...
is_resource
calls
is_resource
function will no longer return true
on return values of any of the OpenSSL extension functions.
Existing code that used is_resource
function to check if the provided value is a valid OpenSSL resource will now need to check against the class names (OpenSSLAsymmetricKey
, OpenSSLCertificate
, and OpenSSLCertificateSigningRequest
, depending on the use case) as well.
Using $value !== false
can be used to check the validity as well, and works across all PHP versions.
Deprecation and resource freeing
In PHP 8.0, the following functions are deprecated.
Both of these functions are deprecated because the new objects automatically close the internal handlers when the objects fall out of scope, or when the request terminates.
Backwards Compatibility Impact
All OpenSSL functions return and accept class objects in lieu of the resource
counter-parts in older PHP versions.
Unless there are is_resource
calls on OpenSSL extensions resource
types (pre-PHP 8.0), this update should not introduce any issues.
Note that openssl_pkey_free
and openssl_x509_free
functions a deprecated in PHP 8.0, and causes deprecation warnings in PHP 8.0. In code that must be backwards-compatible on PHP versions prior to 8.0, those functions needs to be called conditionally to avoid the deprecation notice.
- openssl_pkey_free($key);
+ if (\PHP_VERSION_ID < 80000) {
+ openssl_pkey_free($key);
+}
`XMLParser` objects replace `xml` resources
As part of PHP's resource
to object migration, XML Parser extensions primary resource
types are migrated to XMLParser
class objects in PHP 8.0.
Prior to PHP 8.0, xml_parser_create
and xml_parser_create_ns
functions returned a resource
of type xml
. In PHP 8.0, all XML Parser functions return and accept XMLParser
objects.
XMLParser
class synopsis
final XMLParser {}
xml_parser_create
and xml_parser_create_ns
functions must be used to instantiate XMLParser
objects. Directly instantiating with new XMLParser
construct is not allowed, and results in an error:
new XMLParser();
PHP Error: Cannot directly construct XmlParser, use xml_parser_create() or xml_parser_create_ns() instead in ... on line ...
is_resource
calls
The return values of xml_parser_create
and xml_parser_create_ns
functions are standard PHP class objects, and are no longer resource
objects.
is_resource
function no longer returns true
for the return values of said functions.
Destroying XMLParser
objects
It is no longer necessary to call xml_parser_free
that was required in previous PHP versions to avoid memory leaks.
xml_parser_free
function is not deprecated, and calling it (for backwards-compatibility) will not cause any issues.
Note that it may be necessary to explicitly destroy XML parser objects (unset($parser)
) in case the objects might not fall out of scope for the garbage collector to clean them. PHP 8's Weak Maps might help to associate additional data for XML Parser objects without objecting the garbage collector.
Backwards Compatibility Impact
Unless there are is_resource
calls, there should be no backwards-compatibility issues. In PHP 8.0, all XML Parser functions accept XMLParser
objects instead of resource
objects.
Internal function warnings now throw `TypeError` and `ValueError` exceptions
In PHP 8, internal function parameters have types and value validations enforced, and will throw \TypeError
or \ValueError
exceptions if the expected type or value is not allowed.
Prior to PHP 8, this resulted in a PHP warning.
Not all PHP warnings emitted by internal functions are transformed to exceptions, but majority of the functions will throw \TypeError
or \ValueError
exceptions if the provided type is not allowed, or the provided value is invalid. This includes functions that accept multiple types (such as a string or an array) because PHP 8 comes with Union Types.
Out of all PHP 8 changes, this will likely be the biggest pain-point when you upgrade existing code.
Reasons behind this decision
Many of the PHP internal functions gracefully handle unexpected values by raising a PHP warning, but still returning a "false-ish" value such as null
, false
, or 0
. This can lead to subtle bugs that are later discovered, if discovered at all, in different parts of the program.
For example, json_decode()
function accepts a $depth
parameter that must be a positive integer. This was not enforced with an exception prior to PHP 8. If you call json_decode()
with an invalid $depth
, json_decode()
function will raise a warning, but still return null
, which is an acceptable return type of the original json-encoded value is also null
.
With types and values enforced, json_decode()
function throws an exception when it encounters an unexpected type or a value.
This can result in applications that dismissed the warning prior to PHP 8 to fail due to the unexpected exception. However, this results in fewer bugs once fixed because PHP makes sure to defend aggressively against invalid values.
\TypeError
Examples
Warning to Exception
Most of the PHP internal functions that accept a typed parameter now throw \TypeError
exceptions instead of warnings. This can eliminate a lot of subtle bugs because most of these string functions return either false
or null
on such unexpected types, which can result in a bug somewhere else.
substr('foo', []);
PHP versions prior to 8 will raise a warning and return null
instead of throwing a \TypeError
and refusing to go forward. The type is now enforced and throws exceptions in PHP 8.
- Warning: substr() expects parameter 2 to be int, array given in ... on line ...
+ Fatal error: Uncaught TypeError: substr(): Argument #2 ($start) must be of type int, array given in ...:...
New \TypeError
without prior warnings
Some functions, such as method_exists()
did not throw exceptions on unexpected values, but returned a value that fulfills the semantic return values of the function.
Union types are used when they are deemed necessary. For example, method_exists()
function accepts either a class name (string
) or an object (object
). This is enforced as a Union Type of string|object
.
method_exists([], 'getName');
This will now throw a \TypeError
. Prior to PHP 8, it returned false
if the provided parameter is not a string or an object, but did not raise any warnings.
+ Fatal error: Uncaught TypeError: method_exists(): Argument #1 ($object_or_class) must be of type object|string, array given in ...:...
\ValueError
examples
PHP throws \ValueError
exceptions if the provided value is of correct type, but not acceptable in the context.
json_decode('"foo"', true, -1);
Prior to PHP 8, setting a depth less than 0 resulted in a warning, which is now promoted to a \ValueError
exception:
- Warning: json_decode(): Depth must be greater than zero in ... on line ...
+ Fatal error: Uncaught ValueError: json_decode(): Argument #3 ($depth) must be greater than 0 in ...:...
Many SPL functions, mbstring_
functions, password_*
functions, etc now throw \ValueError
exceptions when the provided values cannot be used to go further.
Backwards compatibility impact
Except for certain cases such as method_exists
, the \TypeError
and \ValueError
exceptions PHP 8 and later throws resulted in a warning prior to PHP 8.
Unless you went out of your way to silent (e.g @strlen([])
) the warning, or dismissed in from the error log, you should not encounter any major problems.
Except for the few exceptional cases, all the changes you make because of the new \TypeError
and \ValueError
exceptions will work all the same in prior PHP versions as well.
The new \ValueError
exception class can be polyfilled, but that does not mean internal functions will throw exceptions instead of raising warnings.
class ValueError extends Error {}
It is possible to have a try/catch
block that accepts \ValueError
exceptions without having the \ValueError
exception class declared. It will not be thrown in prior versions, which makes the try/catch
block a harmless no-op in older PHP versions.
try {}
catch (\ValueError $exception) {}
Expressions can now `throw` Exceptions
Prior to PHP 8.0, it was not allowed to throw an exceptions in when a single expression is expected. It is now possible to throw an exception in arrow functions, ternary expressions, or anywhere else the PHP parser expects a single expression.
Arrow Functions:
$fn = fn() => throw new \Exception('oops');
$value = isset($_GET['value'])
? $_GET['value']
: throw new \InvalidArgumentException('value not set');
$value ??= throw new \InvalidArgumentException('value not set');
$foo = $bar ?: throw new \InvalidArgumentException('$bar is falsy');
$foo = $bar ?? throw new \InvalidArgumentException('$bar is not set');
All of the snippets above are allowed since PHP 8.0.
Versions prior to 8.0 trigger a parse error:
Parse error: syntax error, unexpected 'throw' (T_THROW) in ... on line ...
Backwards Compatibility Impact
Using throw
in an expression previously triggered a parse error, so there should be no practical breaking changes when you upgrade to PHP 8.
However, note that there is no way to make any code that throw
in an expression work seamless in PHP versions prior to PHP 8. If compatibility with older versions is a concerns hold off using throw
in expressions.
JSON extension is always available
A subtle yet healthy change in PHP 8.0 is that the JSON extension is always included in PHP.
Prior to PHP 8.0, a compilation flag ./configure --disable-json
allowed disabling the JSON extension.
Debian, Ubuntu, CentOS/RHEL distributions, and Ondrej's builds included JSON extension by default. In these distributions, it was possible to disable JSON extension (although not a sensible decision due to how widely JSON is used out there). Windows builds on php.net are statically bundled with JSON, which means it is not even possible to disable JSON extension.
For PHP builds that had the unlikely flag --disable-json
in the configuration step, you will need update the build scripts to not use this flag for PHP 8.0 and forwards.
Because the JSON extension is already included in PHP core, you will no longer need to check the extension availability with extension_loaded('json')
, or function_exists('json_decode')
functions because they will be always available. These function calls are not broken; they will always return true
in PHP 8.
For composer.json
file that had ext-json
in the require
section, this requirement is no longer necessary if already requires php ^8.0
.
{
"require": {
"php": "^8.0",
- "ext-json": "*",
}
}
`catch` exceptions only by type
PHP 8.0 and later allows to use try/catch
blocks where the catch()
statement does not catch the exception itself to a variable.
Prior to PHP 8.0, a typical PHP try/catch
block must capture the exception in the catch
statement:
try {
// try something
}
catch (\InvalidArgumentException $ex) { // "$ex" is required
// handle the exception
}
Sometimes, the exception type (such as \InvalidArgumentException
) is enough to determine the way the exception is handled, and capturing the exception to a variable (such as $ex
in the example above), PHP 8.0 allows to drop the exception capturing.
try {
$container->get('api-keys.http2-pro');
}
- catch (NotFoundExceptionInterface $exception) {
+ catch (NotFoundExceptionInterface) {
$logger->log('API key not set');
}
A word of caution
Make sure that the exception type you catch
is granular enough to convey the meaning of the exception. For example, catching a wildcard \Exception
or \Throwable
without capturing the exception might be a bad idea if you intend to log the event.
Backwards Compatibility Impact
PHP versions prior to 8.0 will raise a fatal error with the new syntax:
Parse error: syntax error, unexpected ')', expecting '|' or variable (T_VARIABLE) in ... on line ...
It is not possible back-port this functionality to earlier PHP versions.
Related Changes
- Hierarchy of PHP exceptions
- Internal function warnings now throw
TypeError
andValueError
exceptions - Expressions can now
throw
Exceptions
`+`/`-` operators take higher precedence when used with concat (`.`) operator
PHP 7.3 deprecated unparenthesized expressions containing '.' and '+'/'-'. From PHP 8.0, operator precedence is enforced, and this deprecation notice is no longer raised.
When an expression contains the contact operator (.
) and +
/-
operators, +
/-
operators take precedence.
For example, consider the following snippet:
echo 35 + 7 . '.' . 0 + 5;
Prior to PHP 8, this snippet will be evaluated in the order of the operators.
From PHP 8.0 and later, the +
and -
operators take a high a precedence. If you have been ignoring the deprecation notices since PHP 7.3, you can have different results:
Pre-PHP 8
echo 35 + 7 . '.' . 0 + 5;
// Deprecated: The behavior of unparenthesized expressions containing both '.' and '+'/'-' will change in PHP 8: '+'/'-' will take a higher precedence in /in/n3lVl on line 3
// 47
PHP 8 and later
echo 35 + 7 . '.' . 0 + 5;
// 42.5
This is equivalent to (35 + 7) . '.' . (0 + 5)
.
Backwards compatibility impact
If you have any existing code that raised a deprecation notice due to expressions that contained .
and +
/-
operators in same expression without parenthesis, note that the output of in PHP 8 can be different.
Make sure to properly add paranthesis to your existing expressions to express their meaning.
- echo 35 + 7 . '.' . 0 + 5;
+ echo (35 + 7) . '.' . (0 + 5);
`CurlHandle` class objects replace curl handlers
One of the long-term goals in PHP is to convert resource
type to appropriate class objects. In PHP 8, the Curl functionality is transformed into class object based resources.
Resource to Object Migration
PHP is gradually phasing out allresource
types with class objects, and this migration is one step of the Resource to Object Migration plan.
Init functions return objects
Curl init functions curl_init
, curl_multi_init
, and curl_share_init
functions returned PHP resources prior to PHP 8. It was not possible enforce typing in those functions, or any other user-land functionality that used them.
From PHP 8, these functions return objects. You still have to use init functions to create these objects; it is not allowed to new CurlInit()
(or any other objects for that matter).
curl_init()
now returns a \CurlHandle
object
Prior to PHP 8, when you call curl_init()
, it returned a PHP resource
of type Curl
.
From PHP 8 and forward, curl_init
function returns an instance of \CurlHandle
class.
- /**
- * @return resource|false
- */
- function curl_init() {}
+ function curl_init(): \CurlHandle|false {}
final class CurlHandle {}
curl_multi_init()
now returns a CurlMultiHandle
object
Similar to curl_init()
in PHP 8, curl_multi_init()
now returns a \CurlMultiHandle
object instead of a resource
with type curl_multi
.
- /**
- * @return resource
- */
- function curl_multi_init() {}
+ function curl_multi_init(): \CurlMultiHandle {}
final class CurlMultiHandle {}
curl_share_init()
now returns a \CurlShareHandle
object
curl_share_init()
now returns a \CurlShareHandle
object instead of a resource
with type curl_share
.
- /**
- * @return resource
- */
- function curl_share_init() {}
+ function curl_share_init(): \CurlShareHandle {}
final class CurlShareHandle {}
All these newly added classes, CurlHandle
, CurlMultiHandle
, and CurlShareHandle
follow resource-to-object semantics.
-
Directly instantiating objects is not allowed. For example,
new CurlHandle
will result in an error:new CurlHandle(); new CurlMultiHandle(); new CurlShareHandle();
Cannot directly construct CurlHandle, use curl_init() instead in ... on line ... Cannot directly construct CurlMultiHandle, use curl_init() instead in ... on line ... Cannot directly construct CurlShareHandle, use curl_share_init() instead in ... on line ...
-
All classes are declared final. Extending them is not allowed:
class Foo extends CurlHandle {}
Class Foo may not inherit from final class (CurlHandle) in ... on line ...
-
No dynamic properties are allowed. Attempting to set a dynamic property will result in an error:
$ch = curl_init(); $ch->foo = 'Bar';
Cannot create dynamic property CurlHandle::$foo in ... on line ...
All curl_*
functions accept Curl*
objects instead of resources
PHP does not have a resource
type, so it was not possible to enforce a parameter or return type for any of the curl_
functions. This conveniently makes this resource
to Curl*
object transform not introduce backwards-incompatibilities.
is_resource()
returns false
on Curl*
objects
Because the Curl init function return values are standard PHP objects, is_resource
function returns false
which is the correct bahavior for that function.
This might break your existing code because it is common to check if the curl_init()
call was successful with a is_resource($handle)
call.
Note that in PHP 8 and in old versions, curl_(multi_|share_)init()
functions return false
on failed resource creation.
To make sure the code is compatible with both PHP 8 and older versions, you can change is_resource($handle)
calls with $handle !== false
calls.
$handle = \curl_init();
- if (!\is_resource($handle)) {
+ if ($handle === false) {
throw new \Exception();
}
curl_close
no longer closes the resource
Because Curl resources are now objects, Curl handles are closed when the object is no longer referenced, or explicitly destroyed.
You can still call curl_close($handle)
, but this function no longer has any effect.
In your existing code, you can explicitly unset()
the CurlHandle
object. PHP will automatically do it following standard object garbage collection mechanisms.
$handle = curl_init();
// ... use it ...
curl_close($handle);
+ unset($handle);
Backwards compatibility impact
Calling is_resource()
on Curl init function return values will now return false
, which breaks existing code. For cross-version compatibility, always use $handle === false
which will correctly evaluate in all PHP versions.
PHP has no resource
type declared in a way that you can enforce it in user-land code. This mitigates any possible typed functions.
Furthermore, curl_close
no longer effectively closes the Curl handler. Because the handlers are now objects, the resources will be destroyed during garbage collection, unless the handler is explicitly closed with an unset()
call. This unset()
call can be used in older PHP versions without breaking the functionality. See the example in curl_close
section above
It is not possible to backport this functionality to older PHP versions. This means that if you start to enforce CurlHandle
, CurlShareHandle
, and CurlShareHandle
types, that code will not work in older PHP versions.
Fatal errors on incompatible method signatures
Prior to PHP 8, PHP was inconsistent the way it handled method signature checks when a parent class was extended.
Extending class methods
class Foo {
public function process(stdClass $item): array{}
}
class SuperFoo extends Foo{
public function process(array $items): array{}
// ^^^^^ mismatch
}
In the snippet above, SuperFoo::process
method has a mismatching signature to its parent Foo
. This is a clear violation of Liskov Substitution Principle (LSP), but PHP only raised warning:
Warning: Declaration of SuperFoo::process(array $items): array should be compatible with Foo::process(stdClass $item): array in ... on line ...
In PHP 8, such signature mismatches result in fatal error.
Fatal error: Declaration of SuperFoo::process(array $items): array must be compatible with Foo::process(stdClass $item): array in ... on line ...
Note that this fatal error will only be triggered if it violates LSP. Changing the type of method signatures is allowed as long it follows LSP.
Implementing abstract trait methods
Prior to PHP 8, PHP did not enforce any signature checking when a trait method is extended. In PHP 8.0 and later, abtract
methods with mismatching signatures will fail with a fatal error.
trait Foo {
abstract public function inParams(stdClass $item): array;
abstract public function inReturn(stdClass $item): int;
}
class SuperFoo{
use Foo;
public function inParams(array $items): array{}
// ^^^^^ Mismatch
public function inReturn(stdClass $item): int{}
// ^^^ Mismatch
}
In PHP 8, the snippet above will result in a fatal error:
Fatal error: Declaration of SuperFoo::inParams(array $items): array must be compatible with Foo::inParams(stdClass $item): array in ... on line ...
Note that this fatal error will only be triggered if it violates LSP. Changing the type of method signatures is allowed as long it follows LSP.
PHP LSP enforcement
This change will finally bring PHP 8 to actively enforce signature checks on all extent/implement patterns:
The following chart describes how PHP versions prior to 8.0 enforced signature mismatches, and how it changes in PHP 8
PHP <8 | PHP >=8 | |
---|---|---|
class implements interface : method parameters |
Fatal Error | Fatal Error |
class implements interface : return type |
Fatal Error | Fatal Error |
class extends abstract method: method parameters |
Fatal Error | Fatal Error |
class extends abstract method: return type |
Fatal Error | Fatal Error |
class extends class: Method parameters |
Warning | Fatal Error |
class extends class: Method return type |
Fatal Error | Fatal Error |
trait use and extend: Method parameters |
none | none |
trait use and extend: Method return type |
none | none |
trait use and implement: abstract Method parameters |
none | Fatal Error |
trait use and implement: abstract Method return type |
none | Fatal Error |
Backwards compatibility impact
If you had classes that extended a parent class with mismatching signatures, they will now fail with a fatal error. Prior PHP 8, those errors raised a warning.
If your PHP 7 code base does not raise such warnings, you will be able to upgrade to PHP 8 without problems. Fixing the code for PHP 8 will fix the warning in PHP 7 as well.
abtract
trait method mismatches did not emit any warnings whatsoever in any PHP versions prior to PHP 8, and they will fail with a fatal error in PHP. As pointed in a Reddit discussion, this change can introduce unexpected errors that are not currently detected by any code analysis tools because that was technically allowed.
Symfony, for example, used this pattern that needed to be fixed.
Class method fatal errors
abstract
trait method fatal errors
Disabled functions behave as if they do not exist
PHP has a feature to disable certain functions and classes with disable_functions
and disable_classes
INI directives. This is often used as a security measure to disable potentially unsafe functions.
This functionality still exists in PHP 8, but the disabled functions behave as if they are not declared at all.
Prior to PHP 8, attempting to use a disabled functions resulted in a warning:
Warning: substr() has been disabled for security reasons in ... on line ...
In PHP 8 and later, attempting to use a disabled function will throw a standard error for an undeclared function:
Fatal error: Uncaught Error: Call to undefined function substr() in ...:...
This change makes the disable feature transparent to consumers, and it even allows the function to be redefined. Prior to PHP 8, attempting to redefine a disabled function resulted in a fatal error.
disable_functions=substr
if (!function_exists('substr')) {
function substr() {}
}
Fatal error: Cannot redeclare substr() in ... on line ...
function_exists()
returns false
for disabled functions
function_exists()
function now returns false
for functions disabled with disable_functions
INI directive. This is because internally, disabled functions do not even make to the internal functions table.
Prior to PHP 8, to check if a function is disabled, one would need to query the INI setting, or use the return value of get_defined_functions()
function.
Disabled functions can be redefined
In PHP 8, it is possible to redefine a disabled function.
This enables a whole new set of ways to polyfill and mock disabled function. Test frameworks and mocking frameworks will be able to dynamically disable a function, and redefine it user-land as a mock, and polyfill libraries will be able to provide a user-land implementation for functions that are disabled.
// test.php
if (function_exists('random_int')) {
return 4;
}
If you want to mock the random_int
function, now you can execute the test script with:
php -d disable_functions=random_int test.php
random_int
will now always return 4
as if it was chosen by fair dice roll.
Function re-declaration and mock/polyfill exampls are covered in PHP 8: Override internal functions with disable_functions
.
Deprecation notices
Related to this change, there are two new deprecation notices:
ReflectionFunction::isDisabled()
get_defined_functions()
with$exclude_disabled
has no effect
These changes are detailed in Disabled functions: Reflection and get_defined_functions()
deprecations
Backwards compatibility impact
Redefining a disabled function in user-land code was not allowed in PHP versions prior to 8.0. This means you will be able to upgrade existing code, but any code that intentionally redefined a function to work-around disabled_functions
feature will not work in older PHP versions.
`GdImage` class objects replace GD image resources
From PHP 8.0 and forward, GD extension uses \GdImage
class objects instead of resource
s for its underlying data structures.
This is similar to Curl extension using \CurlHandle
objects as opposed to resource
in PHP 8.0.
\GdImage
class
final class GdImage{
}
\GdImage
is declared in global namespace.- Declared
final
. - Has no methods.
- Cannot be instantiated with
new \GdImage()
. - Cannot be cloned with
clone $image
. - Cannot be serialized.
GD functions return and accept \GdImage
objects instead of resource
All GD functions that accepted resource
values parameters prior to PHP 8.0 will now accept \GdImage
objects. Similarly, their return values will be \GdImage
objects instead of resource
.
is_resource()
calls
Note that all functions in GD extension now returns and expects \GdImage
objects instead of resource
objects. Unless you explicitly call is_resource()
function on return values of GD functions (such as imagecreatefromjpeg()
and imagecreate()
), this change will be completely trouble-free.
If you are calling is_resource
, you will now need to account for the \GdImage
object return type too.
- if (is_resource($image)) {
+ if (is_resource($image) || $image instanceof \GdImage) {
A previous version of this post recommended to use
is_object
beforeinstanceof
calls on PHP versions prior to 7.2. It is not necessary to useis_object
calls withinstanceof
operator as long as they are not literals, as shown in the example above.
imagedestroy()
is no-op
imagedestroy()
function is still available in PHP 8.0, but it does nothing. Memory and other resources will be freed when the \GdImage
object is removed either with an explicit unset()
call or when it falls out of scope.
Gd font
The remaining GD font
resource is migrated to a class object in PHP 8.1: Font identifiers are \GdFont
class objects.
Backwards Compatibility Impact
GD extension's resource
to object
migration is quite transparent. It will not be necessary to polyfill the new GdImage
class because the internal nature of this change.
Existing code that uses is_resource()
will now need either change to check the class name of the object to support PHP 8.0.
Assertions throw exceptions by default
PHP supports Assertions with the assert()
language construct since PHP 4. Until PHP 8.0, a failed assertion raised a warning by default, but not an exception.
assert(true === false);
// Warning: assert(): assert(true === false) failed in ... on line ...
To make the engine throw an exception, an INI option is needed:
[PHP]
assert.exception=1
In PHP 8.0, the default value for assert.exception
INI value is changed from 0
to 1
. This means failed assertions will throw an AssertionError
exception by default.
assert(true === false);
// Fatal error: Uncaught AssertionError: assert(true === false) in ...:...
assert.warning
INI value remains 1
, but assert.exception=1
means an exception will be throw instead of a warning.
Assertion callback
There are no changes in the Assertion failure callback functionality.
Assertion callback will continue to receive the same information.
;INI configuration:
[Assertion]
assert.callback=assert_fail
// assert_options() can be used instead of an INI configuration.
// assert_options(ASSERT_CALLBACK, 'assert_fail');
assert(true === false);
function assert_fail(string $file, int $line , string $assertion, string $description = ''): void {
// Do something with the failed assertions.
}
Assertion failure callback will not receive the AssertionError
exception. Standard exception handling can be used to catch AssertionError
exceptions. This is the same behavior as previous versions.
It is also possible to use ASSERT_CALLBACK
/assert.callback
callback without any further warnings or exceptions with an INI setting:
assert.exception=0
assert.warning=0
assert.callback=assert_fail
Backwards Compatibility Impact
Assertions are not meant to be used in production code. Throwing an exception by default in PHP 8.0 means assertion failures will be front and center.
Restoring the original functionality, although not recommended, is possible with an INI setting.:
[Assertion]
assert.exception=0
assert.warning=1
Sockets extension resources (`Socket` and `AddressInfo`) are class objects
In PHP 8.0 and later, all functions from Sockets extension return/accept objects of type \Socket
and \AddressInfo
class instead of resource
of type Socket
and AddressInfo
.
This is similar to Curl extension using \CurlHandle
objects as opposed to resource
in PHP 8.0, and GD extension using \GdImage
objects as opposed to gd
resources.
Note that this only applies to
socket_*()
functions fromext/sockets
.stream_socket_*
functions (which returnstream
resources) are not affected.
\Socket
class
final class Socket{
}
\Socket
class objects replace return values of functions that formerly returned a resource
of type Socket
:
socket_accept()
socket_addrinfo_bind()
socket_addrinfo_connect()
socket_create()
socket_create_listen()
socket_import_stream()
socket_wsaprotocol_info_import()
Socket
resources can be closed with the socket_close()
function, which is still necessary in PHP 8.0 as well to promptly close an open socket.
\AddressInfo
class
final class AddressInfo {
}
An array of \AddressInfo
class objects will be returned by socket_addrinfo_lookup()
function, which formerly returned an array of AddressInfo
resources.
\Socket
and \AddressInfo
class semantics
- Both classes are declared in the global namespace.
- Declared
final
. - Has no methods.
- Cannot be instantiated with
new \Socket()
andnew AddressInfo()
. - Cannot be serialized.
- Cannot be cloned.
Attempting to instantiate a \Socket
object will throw a fatal error:
PHP Error: Cannot directly construct Socket, use socket_create() instead in ... on line ...
socket_*()
parameters and return values
All functions from Sockets extension will return/accept the new \Socket
and \AddressInfo
class objects.
is_resource()
calls
All functions that create resources/class objects from the Sockets extension return false
if the operation was unsuccessful.
Although not necessary, using is_resource()
on the return value of such functions is quite common.
If you are calling is_resource
, you will now need to account for the \Socket
object return type too.
- if (is_resource($socket)) {
+ if (is_resource($socket) || $socket instanceof \Socket) {
It is also completely valid to check the return value of functions that return resources to check the return value for false
.
- if (is_resource($socket)) {
+ if ($socket !== false) {
Backwards Compatibility Impact
Socket extensions resource
to object transformation is quite seamless. Unless you have an is_resource()
call that checks a passed/return value, it will not require any changes in PHP 8.0.
PostgreSQL: Several aliased functions are deprecated
PHP 8 deprecates several function aliases in PostgreSQL extension. These deprecated functions do not appear in the PHP documentation today, and all code that appear to use them are from the PHP 4 era.
However, there are several positive search results on GitHub, which hints of possible deprecation notices in PHP 8.0.
A total of 24 functions are deprecated, all of which were not documented. Last appearance of these functions in documentation appears to be from 2001.
Deprecated Function | Replace with |
---|---|
pg_clientencoding |
pg_client_encoding |
pg_cmdtuples |
pg_affected_rows |
pg_errormessage |
pg_last_error |
pg_fieldisnull |
pg_field_is_null |
pg_fieldname |
pg_field_name |
pg_fieldnum |
pg_field_num |
pg_fieldprtlen |
pg_field_prtlen |
pg_fieldsize |
pg_field_size |
pg_fieldtype |
pg_field_type |
pg_freeresult |
pg_free_result |
pg_getlastoid |
pg_last_oid |
pg_loclose |
pg_lo_close |
pg_locreate |
pg_lo_create |
pg_loexport |
pg_lo_export |
pg_loimport |
pg_lo_import |
pg_loopen |
pg_lo_open |
pg_loread |
pg_lo_read |
pg_loreadall |
pg_lo_read_all |
pg_lounlink |
pg_lo_unlink |
pg_lowrite |
pg_lo_write |
pg_numfields |
pg_num_fields |
pg_numrows |
pg_num_rows |
pg_result |
pg_fetch_result |
pg_setclientencoding |
pg_set_client_encoding |
Backwards Compatibility Impact
Deprecated functions above will raise a deprecation notice:
Deprecated: Function pg_numrows() is deprecated in ... on line ...
A simply function swap with canonical function will be the straight-forward fix. All the canonical functions are already available in PHP versions 4.2 through 8.0 and future.
Deprecate required parameters after optional parameters in function/method signatures
When declaring a function or a method, adding a required parameter after optional parameters is deprecated since PHP 8.0.
This means the following function signature triggers a deprecation notice:
function foo($param_optional = null, $param_required) {
// ^^ optional parameter , ^^ required parameter
}
From PHP 8.0 onwards:
Deprecated: Required parameter $param_required follows optional parameter $param_optional in ... on line ...
If you have a require parameter (i.e. a parameter without a default value set in its signature) after an optional one (i.e. a parameter with a default value), it makes all parameters before it essentially required because the caller has to explicitly pass a value for optional parameters as well.
To quote a bug submitted to bugs.php.net back in 2010:
PHP does not emit a notice of any kind when defining a function with a required
parameter after an optional parameter. For example:
function foo($optional = 1, $required) {}
It doesn't make sense to define a required parameter after an optional one,
since that effectively makes all preceding optional parameters required. Since this is
an error that can produce bugs and other warnings if one is not careful (calling
the above function with less than two parameters will cause warnings to be
emitted for the missing $required parameter), PHP should emit a warning of some kind
when functions like this are defined.
PHP documentation already explains that having required parameters after optional parameters is incorrect. There was no deprecation notice until PHP 8.0, even though it hints a likely issue in the code architecture.
Nullable parameters
If you have used typed parameters with a default value set to null
, you can use nullable types instead. To provide an example:
function foo(string $param_optional = null, $param_required) {
// ^^ poormans nullable param , ^^ required parameter
}
This "trick" was used with PHP 7.0, there was no nullable type support until PHP 7.1. The snippet above can be replaced with the following:
function foo(?string $param_optional, $param_required) {
// ^^ optional parameter , ^^ required parameter
}
The deprecation does not apply if there is a type declared on the optional parameter, and the default value is null
.
The following will not trigger a deprecation notice:
function foo(string $param_optional = null, $param_required) {}
This is because there is a type (string
) declared for the optional $param_optional
parameter, and its default value is null
. Setting any other default value (such as $param_optional = 'bar'
), or not declaring the type will trigger the deprecation notice.
Thanks to M1keSkydive and the_alias_of_andrea for the insight to extend this part.
Backwards compatibility impact
If you have any functions/methods that have required parameters after optional parameters, you will get a deprecation notice similar to the one in the example. It is most likely an indication of poorly defined architecture. The deprecation notice is triggered at the compile time, and it will be raised even if it is not called.
You can work around it by removing the default value in all optional parameters before the last required parameter. This should not break your existing code because all callers must pass a value to the optional parameters before proving values for the required parameters that appear later.
`ReflectionParameter::getClass())`, `::isArray()`, and `::isCallable()` methods deprecated
PHP 8 introduces several improvements in PHP type systems such as the introduction of Union Types, mixed
type, and a few more.
With these changes, certain methods in Reflection API's ReflectionParameter
yield incorrect results.
In PHP 8, the following methods from ReflectionParameter
class is deprecated:
ReflectionParamter::getType()
is the recommended way to replace the deprecated methods. This method is available in PHP 7.0 and later.
ReflectionParameter::getClass()
deprecation
Trying to use ReflectionParameter::getClass()
will emit a deprecation notice in PHP 8:
Deprecated: Function ReflectionParameter::getClass() is deprecated in ... on line ...
ReflectionParameter::getClass()
returns the name of the class of a parameter. However, when a Union Type is used, it returns null
if difference class types are in a single Union.
ReflectionParameter::getType()
supersedes getClass()
method. Note that getClass()
method returns a ReflectionClass()
object, which implements __toString()
. If you are interested in just the name, you can use $param->getType()->getName()
to return the name.
A full replacement with same functionality:
- $name = $reflectionParam->getClass();
+ $name = $param->getType() && !$param->getType()->isBuiltin()
+ ? new ReflectionClass($param->getType()->getName())
+ : null;
ReflectionParameter::isArray()
deprecation
ReflectionParameter::isArray()
is deprecated in PHP 8 because it only works with array
and ?array
types, but not with a Union type.
ReflectionParameter::getType()
result can be used to mimic the result of this method:
- $isArray = $param->isArray();
+ $isArray = $param->getType() && $param->getType()->getName() === 'array';
ReflectionParameter::isCallable()
deprecation
ReflectionParameter::isCallable()
is deprecated in PHP 8 too, and similar to isArray()
, it only works for callable
and ?callable
parameters, but not when it's used in a Union Type.
ReflectionParameter::getType()
result can be used to mimic the result of this method for isCallable
as well:
- $isCallable = $param->isCallable();
+ $isCallable = $param->getType() && $param->getType()->getName() === 'callable';
Backwards compatibility impact
All 3 deprecated methods can be easily replaced with the result of getType()
method, which is more robust and accurate in PHP 8. getType()
method is available in PHP 7.0 and later.
If you need your code to work in PHP 5, through 8, conditional code branches depending on the PHP version can be used. For all other cases, replacing the deprecated method with getType()
approaches will be backwards compatible to all PHP 7 versions as well.
Disabled functions: Reflection and `get_defined_functions()` deprecations
In PHP 8, disabled functions are not added to PHP's internal functions table. This makes PHP behave as if disabled functions are not defined at all.
Related to this change, there are two new deprecations in PHP 8.
ReflectionFunction::isDisabled()
is deprecated
ReflectionFunction::isDisabled()
method is deprecated because it no longer provides any useful information to the caller.
Deprecated: Function ReflectionFunction::isDisabled() is deprecated in %s on line %d
This method will always return false
even if the function is disabled with the INI directive.
There is no need to check if a function is disabled because function_exists()
will return true
only if the function is declared and not disabled.
get_defined_functions($exclude_disabled = false)
is deprecated
For the same reasons that disabled functions are not included in functions list, get_defined_functions()
function with the first parameter $exclude_disabled
set to false
is disabled.
get_defined_functions()
will always exclude disabled functions in PHP 8, and even if you try to get all functions including the excluded ones, it will not return disabled functions.
If you redefine a disabled function, that function will be in the user
key of the get_defined_functions()
return array, even if it is defined internally and disabled via the disable_functions
INI directive.
Backwards compatibility impact
The main use-case for disable_functions
INI directive is to disable potentially unsafe functions such as system()
. The deprecation notices are unlikely to occur, but it does not introduce any loss of functionality.
A fuzzy GitHub code search does not yield any uses this pattern.
`libxml_disable_entity_loader` function is deprecated
XML standard allows external entities, which can refer to other external resources, which often leads severe security vulnerabilities commonly categorized as XXE, or XML EXternal Entities.
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<foo>&xxe;</foo>
In a text-book example above, the payload above will refer to the /etc/passwd
file in the system the XML file is parsed.
Libxml, the library PHP uses to process XML in extensions such as DOM
, XMLWriter
and XMLReader
was vulnerable to XXE attacks unless loading of external entities was disabled:
libxml_disable_entity_loader(true);
Calling libxml_disable_entity_loader()
with a true
/false
value allowed to toggle disabling of this feature.
In PHP 8.0 and later, PHP uses libxml
versions from 2.9.0, which disabled XXE by default. libxml_disable_entity_loader()
is now deprecated.
Backwards Compatibility Impact
Attempting to call this function will now raise a warning:
Deprecated: Function libxml_disable_entity_loader() is deprecated in ... on line ...
On code that only runs on PHP 8.0 and later, it is now safe to remove all function calls.
For versions prior to PHP 8, a conditional call might be the best approach:
- libxml_disable_entity_loader(true);
+ if (\PHP_VERSION_ID < 80000) {
+ libxml_disable_entity_loader(true);
+ }
Alternately, LIBXML_VERSION < 20900
condition can be used for conditional entity loader toggle:
- libxml_disable_entity_loader(true);
+ if (\LIBXML_VERSION < 20900) {
+ libxml_disable_entity_loader(true);
+ }
Note that in
libxml
< 2.9.0, external entity loading is enabled by default. Removinglibxml_disable_entity_loader(true)
calls on environments that run PHP versions < 8.0 will open an XXE vulnerability.
XMLRPC extension is moved to PECL
XMLRPC is an extension that was bundled in PHP that brought XML RPC server and client features to PHP.
This extension was relatively unused, and was marked "experimental" all along. This extension relied on some of the libraries that were not maintained for several years.
xmlrpc
extension is unbundled in PHP 8.0. You can still install the extension from PECL if your code or any of the dependencies require.
Backwards Compatibility
Because xmlrpc
is no longer bundled, you will need to make sure the extension is available.
For composer projects, make sure to add ext-xmlrpc
as a dependency to ensure Composer checks the availability of the extension in its platform checks.
Alternative Libraries
Note that the xmlrpc
library and its dependencies were not updated in several years, and a more apt approach would be to replace it with more modern libraries that rely in xml
extension or work with its own XML implementation:
`FILTER_FLAG_SCHEME_REQUIRED` and `FILTER_FLAG_HOST_REQUIRED` flags are removed
filter_var()
function used to accept two flags that were deprecated in PHP 7.3:
FILTER_FLAG_SCHEME_REQUIRED
FILTER_FLAG_HOST_REQUIRED
filter_var()
function, when used with FILTER_VALIDATE_URL
already assumed these two flags, and it was not necessary to use these flags explicitly.
Deprecated in PHP 7.3, these two flags are now removed in PHP 8.0.
$var = 'https://php.watch';
- filter_var($var, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED);
- filter_var($var, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED);
- filter_var($var, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED | FILTER_FLAG_HOST_REQUIRED);
+ filter_var($var, FILTER_VALIDATE_URL);
If attempted to use these two flags, a fatal error will be thrown:
Fatal error: Uncaught Error: Undefined constant 'FILTER_FLAG_SCHEME_REQUIRED' in ...:...
Fatal error: Uncaught Error: Undefined constant 'FILTER_FLAG_HOST_REQUIRED' in ...:...
Backwards Compatibility Impact
Because the FILTER_FLAG_HOST_REQUIRED
and FILTER_FLAG_SCHEME_REQUIRED
constants are removed in PHP 8, attempting to use them will now throw a fatal error.
The easiest fix is to simply remove these two flags. filter_var()
function when used with FILTER_VALIDATE_URL
will always enforce the host-required and scheme-required condions regardless of the presence of these two flags.