Backward Incompatible Changes
By-reference foreach loops may now visit previously skipped elements after array compaction. Internal pointers on deleted elements now move to the next surviving element during copy-on-write.
PHP Core
By-reference foreach after array compaction
By-reference foreach loops may now visit previously skipped elements after array compaction. Internal pointers on deleted elements now move to the next surviving element during copy-on-write.
Null coalescing and empty() on magic properties
?? and empty() on a magic property no longer call __get() when __isset() has materialized the property by writing into the property table. The freshly-written value is returned directly. isset() applied directly to the property is unaffected; nested forms such as isset($o->p['k']) follow the same path as ??.
<?php
#[\AllowDynamicProperties]
class Example
{
public function __isset(string $name): bool
{
$this->$name = 'from __isset()';
return true;
}
public function __get(string $name): string
{
return 'from __get()';
}
}
$example = new Example();
var_dump($example->foo ?? 'default');
// As of PHP 8.6.0: string(14) "from __isset()"
// Prior to PHP 8.6.0: string(12) "from __get()"String conversion in array_intersect()
array_intersect() with at least two arrays now converts values to strings while scanning its inputs instead of during sort comparisons. This can change the number and order of conversion warnings and __toString() calls, which conversion exception is reached, and the result for stateful __toString() implementations. Argument types are validated before checking for empty arrays or converting values, so an invalid later argument can suppress conversion side effects from earlier arrays. Values are not converted if any input array is empty.
Form feed trimmed by default
Form feed (\f) is now added to the default trimmed characters of trim(), rtrim() and ltrim().
<?php
var_dump(trim("\f hello \f"));
// As of PHP 8.6.0: string(5) "hello"
// Prior to PHP 8.6.0: string(9) "\f hello \f"New errors for invalid arguments
The following functions now raise a ValueError when an invalid flag or mode value is passed:
array_filter()(mode)array_change_key_case()(case)pathinfo()(flags)scandir()(sorting_order)
The following functions now raise a ValueError when the given argument contains NUL bytes:
getenv()andputenv()(first argument)dl()(extension_filename)openlog()(prefix)parse_str()(string)proc_open()(cwd)
The following functions now raise a ValueError for out-of-range values instead of silently clamping them or allowing them to overflow:
number_format(), whendecimalsis outside the integer range.sleep(), whensecondsis greater thanUINT_MAX, orUINT_MAX / 1000on Windows.usleep(), whenmicrosecondsis greater thanUINT_MAX.
linkinfo() now raises a ValueError when the path argument is empty.
setlocale() now raises a ValueError when a locale name contains NUL bytes, instead of silently truncating it. Arrays are now accepted only for the locales argument. Passing an array as a later variadic locale argument now throws a TypeError. Passing any additional locale arguments when locales is an array now throws an ArgumentCountError.
The following functions now raise a ValueError when the filename argument contains NUL bytes:
fileperms()fileinode()filesize()fileowner()filegroup()fileatime()filemtime()filectime()filetype()is_writable()is_readable()is_executable()is_file()is_dir()is_link()file_exists()lstat()stat()
Notice on imprecise number conversion
base_convert(), bindec(), hexdec() and octdec() now raise a notice when they cannot precisely convert the given number.
Endianness modifiers in unpack() format names
unpack() now reads a < or > immediately following a format code as an endianness modifier rather than as the first character of the element name. Formats such as "s<value" now produce the key "value" instead of "<value", and "C>name" raises a ValueError because the C format code accepts no endianness modifier. A name starting with these characters is unaffected when a repeater precedes it, as in "s1<value". See also the new endianness modifiers.
<?php
var_dump(unpack('s<value', "\x01\x00"));
// As of PHP 8.6.0: ["value" => 1]
// Prior to PHP 8.6.0: ["<value" => 1]COM
It is no longer possible to clone variant objects, because the cloning behavior was ill-defined.
cURL
The callback registered with CURLOPT_READFUNCTION now throws a ValueError when returning an integer other than 0, CURL_READFUNC_ABORT or CURL_READFUNC_PAUSE.
DOM
Properties previously documented as read-only (e.g. DOMNode::$nodeType, DOMDocument::$xmlEncoding, DOMEntity::$actualEncoding, DOMEntity::$encoding, DOMEntity::$version) are now declared with asymmetric visibility (public private(set)). Attempts to write to them from outside the class now raise Cannot modify private(set) property class::$prop from global scope instead of the prior readonly modification error. ReflectionProperty::isWritable also reports these properties accurately.
Array access on Dom\DtdNamedNodeMap objects now returns null for negative integer indexes instead of returning the first node, and raises a ValueError when the integer index is greater than PHP_INT_MAX instead of overflowing to a smaller index.
FTP
ftp_nb_fget(), ftp_nb_fput(), ftp_nb_get() and ftp_nb_put() now throw an Error when the connection is already transferring, instead of emitting a warning and returning false. ftp_close() already throws on the same condition.
GD
imagesetstyle(), imagefilter() and imagecrop() filter the types and values of their array arguments and raise a TypeError or ValueError accordingly.
imageaffinematrixget() now enforces the documented array|float type for the options parameter, including the corresponding weak and strict typing behavior.
GMP
gmp_fact() now throws a ValueError if num does not fit into an unsigned long.
gmp_pow(), gmp_binomial(), gmp_root() and gmp_rootrem() now throw a ValueError if their second argument does not fit into an unsigned long.
The shift (<<, >>) and exponentiation (**) operators on GMP objects now throw a ValueError when GMP right operands are outside the unsigned long range, instead of silently truncating them.
GMP integer string parsing now throws a ValueError for strings containing NUL bytes, instead of silently truncating them.
The message of the DivisionByZeroError raised by gmp_powm() on modulo by zero now includes the function name and argument index (modulus).
Intl
Passing a non-stringable object as a time zone to Intl APIs that accept time zone objects or strings now raises a TypeError instead of an Error.
IntlIterator::current now returns null when called before the iterator is positioned, or after the iterator becomes invalid, instead of exposing an undefined value.
IntlBreakIterator::getLocale now raises a ValueError when the type is neither Locale::ACTUAL_LOCALE nor Locale::VALID_LOCALE instead of returning false.
MessageFormatter::parse and MessageFormatter::parseMessage now return PHP_INT_MIN as int, rather than float, on 64-bit platforms when parsing integer values.
The type parameter of IntlBreakIterator::getPartsIterator has been changed from string to int to match the underlying implementation.
UConverter::transcode now rejects from_subst and to_subst option values longer than 127 bytes instead of silently truncating the length before passing it to ICU.
ResourceBundle::get and resourcebundle_get() now report fallback-disabled resource lookups with without fallback to locale instead of the malformed without fallback from to locale.
IntlDateFormatter::parse/datefmt_parse() and IntlDateFormatter::localtime/datefmt_localtime() now raise a TypeError when the offset argument is not of type int instead of silently converting the value.
Collator::sort, collator_sort(), Collator::asort, and collator_asort() now report UTF-8/UTF-16 conversion failures during comparison through the intl error mechanism and return false. With intl.use_exceptions enabled, these failures throw an IntlException. Previously, these paths emitted a warning and compared the value as an empty string.
MBString
Unicode data tables have been updated to Unicode 18.0.
PCNTL
pcntl_alarm() now raises a ValueError if the seconds argument is lower than zero or greater than the platform's UINT_MAX.
pcntl_exec() now raises a ValueError if the args argument is not a list array.
PCRE
preg_grep() now returns false instead of a partial array when a PCRE execution error occurs (e.g. malformed UTF-8 input with the u modifier). This is consistent with the other preg_* functions.
PGSQL
When pg_fetch_object() is given a non-empty constructor_args for a class without a constructor, the ValueError now refers to the constructor_args argument instead of class. Errors raised when the requested class is not instantiable (abstract, interface, enum) now surface before the row is fetched.
Phar
Phar::mungServer now raises a ValueError when an invalid argument value is passed instead of being silently ignored.
Phar::addEmptyDir now rejects "/.phar" paths in addition to ".phar" paths, and raises the same BadMethodCallException for attempts to create the reserved magic ".phar" directory through that form. Non-magic names that merely share the ".phar" prefix are now treated as ordinary directories.
Files are only automatically interpreted as Phar archives when included if ".phar" occurs as an extension in the filename component of their paths. Previously, it could occur in a directory name or as part of an extension such as ".pharma".
Session
Setting session.cookie_path, session.cookie_domain, or session.cache_limiter to a value containing NUL bytes now emits a warning and leaves the setting unchanged. Previously, NUL bytes were silently accepted: for cookie_path and cookie_domain this caused the SAPI to drop the Set-Cookie header; for cache_limiter the value was silently truncated at the NUL byte.
session_module_name() now throws a ValueError if name is a string containing NUL bytes.
session_encode() now returns an empty string instead of false for empty sessions. It only returns false now when the session data could not be encoded. This mainly happens with the default serialization handler if a key contains the pipe (|) character.
When session.lazy_write is enabled and a session handler implements SessionUpdateTimestampHandlerInterface, sessions that were read as empty and remain empty at write time will now trigger SessionUpdateTimestampHandlerInterface::updateTimestamp instead of SessionHandlerInterface::write. Previously, SessionHandlerInterface::write was always called for empty sessions because session_encode() returned false, bypassing the lazy_write comparison. Custom session handlers that rely on SessionHandlerInterface::write being called with empty data (e.g. to destroy the session) should implement the same logic in their SessionUpdateTimestampHandlerInterface::updateTimestamp method.
The defaults of three session INI settings have changed to provide secure behavior out of the box:
- session.use_strict_mode is now
1(was0). Strict mode rejects uninitialized session IDs, mitigating session fixation. Custom session handlers that previously relied on accepting externally supplied IDs without a corresponding storage entry must either implementSessionUpdateTimestampHandlerInterface::validateIdandSessionIdInterface::create_sid, or explicitly set this to0. - session.cookie_httponly is now
1(was0). Session cookies are no longer accessible to JavaScript viadocument.cookie. Applications that read the session cookie from JavaScript must explicitly set this to0. - session.cookie_samesite is now
"Lax"(was unset). Session cookies are no longer sent on cross-site requests other than top-level navigations using safe HTTP methods. Applications that depend on session cookies being sent on cross-site POST submissions must explicitly set this to"None"(and also set session.cookie_secure to1).
SessionHandler::validateId has been added and delegates to the configured save handler. A subclass that declares SessionHandler::validateId without a return type now emits a deprecation notice for the tentative bool return type. A subclass that overrides SessionHandler::open without calling parent::open() keeps its previous behavior and emits a warning when an ID is validated.
Shmop
shmop_open() now raises a ValueError when the key argument is outside the platform's key_t range instead of passing a truncated key to the operating system.
SimpleXML
SimpleXMLElement::__construct now raises a ValueError when the data argument contains NUL bytes, matching simplexml_load_file(). With dataIsURL set, it previously truncated the path at the first NUL byte. Without it, the string went to libxml, which with default options rejects a NUL on current versions but accepts the truncated document on older ones and under LIBXML_RECOVER.
SOAP
The "classmap" option of SoapClient and SoapServer now rejects arrays containing integer keys. Previously, sparse integer-keyed and mixed-keyed arrays could be accepted. SoapClient now throws a TypeError for non-array "classmap" options and a ValueError for arrays containing integer keys, also when the "exceptions" option is disabled.
WSDL/XML Schema parsing now rejects out-of-range integer values for occurrence constraints and integer restriction facets. Negative minOccurs and maxOccurs values are rejected as well.
SOAP encoding errors now report the affected type or failing operation in the error message instead of the generic Encoding: Violation of encoding rules message. Code that compares the exact message may need to be updated.
Sockets
socket_set_option() with SO_ATTACH_REUSEPORT_CBPF now requires an int value and a level of SOL_SOCKET. Any other value type throws a TypeError instead of being coerced, and any other level raises a warning and returns false.
socket_set_option() with SO_ATTACH_REUSEPORT_CBPF and a value of 0 now detaches the reuseport filter through SO_DETACH_REUSEPORT_BPF. It previously used SO_DETACH_BPF, an alias of SO_DETACH_FILTER, which left the reuseport program attached.
Sodium
The password-hashing functions sodium_crypto_pwhash(), sodium_crypto_pwhash_str(), sodium_crypto_pwhash_scryptsalsa208sha256() and sodium_crypto_pwhash_scryptsalsa208sha256_str() now throw a ValueError instead of a SodiumException when an argument is out of range, such as an opslimit or memlimit below the documented minimum. SodiumException is still thrown for internal libsodium failures.
SPL
SplObjectStorage::getHash implementations may no longer mutate any SplObjectStorage instance. Attempting to do so now throws an Error.
SplFileObject::next now advances the stream when no prior SplFileObject::current call has cached a line. A subsequent SplFileObject::current call returns the new line rather than the previous one.
SplFileObject::fgets no longer caches the returned line for subsequent SplFileObject::current calls. SplFileObject::current now re-reads from the current stream position instead of returning the line SplFileObject::fgets just returned.
SplFileObject::next past EOF no longer increments SplFileObject::key without bound. SplFileObject::seek past EOF now produces the same SplFileObject::key value as SplTempFileObject; the two previously returned different values.
DirectoryIterator::key now returns int|string, and DirectoryIterator::current returns string|SplFileInfo|static.
Sysvshm
shm_attach() now raises a ValueError when the key argument is outside the platform's key_t range instead of passing a truncated key to the operating system.
URI
Uri\WhatWg\Url::__construct now sets softErrors to an empty array when URL processing throws an exception, instead of preserving its value. URL validation errors remain available in the Uri\WhatWg\InvalidUrlException::$errors property.
Uri\WhatWg\Url::__construct, Uri\WhatWg\Url::parse, the Uri\WhatWg\Url::with*() methods, and Uri\WhatWg\UrlBuilder::build now report validation errors in the order in which they are detected. This reverses the order of multiple errors compared to PHP 8.5.
Zip
ZipArchive::extractTo now raises a TypeError for the files argument if one or more of the entries is not a string.
ZipArchive::addGlob and ZipArchive::addPattern now raise a TypeError when the "remove_all_path" option is not of type bool, or when the "comp_method", "comp_flags", or "enc_method" options are not of type int (numeric strings are coerced), instead of emitting a warning.
Serializing or unserializing a ZipArchive now throws an Exception. A subclass can override __serialize() and __unserialize() to round-trip through ZipArchive::closeString and ZipArchive::openString.
Zlib
deflate_init() now raises a TypeError when the value for option "level", "memory", "window", or "strategy" is not of type int.
inflate_init() now raises a TypeError when the value for option "window" is not of type int.