PHP 8.6 is a major update of the PHP language, with new features including Partial Function Application, a new Duration class, a Polling API, and a new clamp() function
', + + 'whats_new' => 'What\'s new in 8.6', + 'upgrade_now' => 'Upgrade to PHP 8.6', + 'old_version' => 'PHP 8.5 and older', + 'badge_new' => 'NEW', + 'documentation' => 'Doc', + 'released' => 'Released Nov 19, 2026', + 'key_features' => 'Key Features in PHP 8.6', + 'key_features_description' => 'Faster, cleaner, and built for developers.
', + + 'features_pfa_description' => 'Allows functions to be called with only a subset of the required parameters, deferring the passing of the remaining ones to a later time.
', + 'features_readonly_property_defaults_description' => 'Allows default values for instance readonly properties.
', + 'features_polling_api_description' => 'Scalable I/O multiplexing using epoll, kqueue, and other native mechanisms, available to event loop libraries like ReactPHP and AMPHP.
', + 'features_clamp_description' => 'Ensures that a given value (numeric or otherwise) falls within a specified range.
', + 'features_duration_description' => 'The Duration class represents elapsed time as measured by a stopwatch.
', + 'features_sort_direction_description' => 'SortDirection is an enum with exactly two cases (Ascending and Descending) and no associated behavior.
', + + 'pfa_title' => 'Partial Function Application', + 'pfa_description' => 'Allows functions to be called with only a subset of the required parameters, deferring the passing of the remaining ones to a later time.', + 'readonly_property_defaults_title' => 'Readonly property defaults', + 'readonly_property_defaults_description' => 'Allows default values for instance readonly properties.', + 'polling_api_title' => 'Polling API', + 'polling_api_description' => 'The newIo\Poll API efficiently monitors many streams at once using the best mechanism available on each platform (epoll, kqueue, event ports, or WSAPoll), without the scalability limits of stream_select(). It is designed as a common foundation that event loop libraries such as ReactPHP and AMPHP can build on.',
+ 'clamp_title' => 'New clamp() function',
+ 'clamp_description' => 'Ensures that a given value (numeric or otherwise) falls within a specified range. Otherwise, the nearest boundary value is returned.',
+ 'duration_title' => 'New \Time\Duration class',
+ 'duration_description' => 'The Duration class represents elapsed time as measured by a stopwatch. Its representation is independent of any time and timezone and allows to represent a range of more than 250 years with nanosecond precision. This makes it well-suited to represent timeouts or benchmark timings in an expressive way.',
+ 'sort_direction_title' => 'New SortDirection enum',
+ 'sort_direction_description' => 'SortDirection is an enum with exactly two cases (Ascending and Descending) and no associated behavior.
Learn more about this feature in The PHP Foundation’s blog.
', + 'stream_error_handling_title' => 'Stream Error Handling', + 'stream_error_handling_description' => 'Stream operations can now be configured to throw aStreamException or to fail silently instead of emitting warnings. Errors are reported as structured StreamError objects with a StreamErrorCode enum case, the wrapper name, and the affected path or URL, and can also be retrieved later with stream_last_errors() or passed to a custom error handler.',
+ 'doc_comments_title' => 'DocComments For Function Parameters',
+ 'doc_comments_description' => 'Function and method parameters can now have their own DocComments, which are available via the new ReflectionParameter::getDocComment() method. This allows each parameter to be documented right next to its declaration, without repeating its name and type in a @param tag.',
+ 'isreadable_iswriteable_title' => 'New ReflectionProperty::isReadable() and ReflectionProperty::isWritable() methods',
+ 'isreadable_iswriteable_description' => 'With readonly properties, asymmetric visibility, and property hooks, checking whether a property is public is no longer enough to know whether it can be read or written. The new methods answer this directly for a given scope, and optionally for a specific object, taking into account visibility, initialization state, hooks, and magic methods.',
+
+ 'new_classes_title' => 'Additional features and improvements',
+ 'new_override_constants' => 'The #[\Override] attribute can now be applied to class constants and enum cases.',
+ 'new_enum_debug_info' => 'Enums can now implement the __debugInfo() magic method.',
+ 'new_const_object_writes' => 'Properties of objects stored in constants can now be modified directly, e.g. Config::DEFAULTS->timeout = 30.',
+ 'new_uri_builder' => 'New Uri\Rfc3986\UriBuilder and Uri\WhatWg\UrlBuilder classes build URIs from their components, along with new methods for detecting URI and host types and a Uri\WhatWg\url_percent_encode() function.',
+ 'new_sodium' => 'The Sodium extension adds post-quantum key encapsulation (X-Wing and ML-KEM-768), IP address encryption, and the SHAKE and TurboSHAKE extendable-output functions.',
+ 'new_tls' => 'TLS streams now support session resumption, external pre-shared keys, and TLS 1.3 early data (0-RTT).',
+ 'new_mysqli_quote_string' => 'New mysqli::quote_string() method escapes a string and wraps it in quotes, matching PDO::quote().',
+ 'new_pack_endianness' => 'pack() and unpack() now accept the < and > endianness modifiers on integer and float format codes.',
+ 'new_json_error_location' => 'JSON error messages now include the location of the error in the input.',
+ 'new_error_include_args' => 'The new error_include_args INI setting consistently includes function arguments in error messages.',
+ 'new_intl' => 'New grapheme_strrev() function reverses strings by grapheme cluster, and the new IntlNumberRangeFormatter class formats ranges of numbers.',
+ 'new_zip' => 'New ZipArchive::openString() and ZipArchive::closeString() methods work with in-memory archives.',
+ 'new_performance' => 'Performance improvements include compiling array_map() with first-class callable or partial application callbacks into loops, faster printf() calls, caching of stateless closures, and faster JSON encoding.',
+
+ 'bc_title' => 'Deprecations and backward compatibility breaks',
+ 'bc_return_in_finally' => 'Using return inside a finally block is now deprecated, as it silently discards exceptions and overrides values returned from the try or catch block.',
+ 'bc_constructor_return' => 'Returning a value from __construct() or __destruct() and making them generators is now deprecated. Declaring a nullable return type for __debugInfo() is also deprecated.',
+ 'bc_reserved_identifiers' => 'Using let or is as the name of a class, function, or constant is now deprecated. Using _ as a constant name, namespace as a class constant name, or readonly as a function name is also deprecated.',
+ 'bc_mbregex' => 'The mbstring regular expression functions such as mb_ereg() and mb_split() have been deprecated, as the underlying Oniguruma library is no longer maintained. PCRE functions with the u modifier can be used instead.',
+ 'bc_type_aliases' => 'The is_double(), is_long(), is_integer(), and doubleval() aliases have been deprecated. Use is_float(), is_int(), and floatval() instead.',
+ 'bc_spl_object_hash' => 'spl_object_hash() has been deprecated in favor of spl_object_id(), and spl_classes() in favor of ReflectionExtension::getClassNames().',
+ 'bc_objects_as_arrays' => 'Passing an object where an array is expected to array_walk(), array_walk_recursive(), http_build_query(), mb_convert_variables(), and compression functions and filters is now deprecated. Use get_object_vars() to convert the object first.',
+ 'bc_strcoll' => 'strcoll() and the SORT_LOCALE_STRING sort flag have been deprecated. Use the Collator class from the intl extension instead.',
+ 'bc_ignored_arguments' => 'Passing a third argument to define() is now deprecated, as is calling is_a() or is_subclass_of() with a class name string when $allow_string is false.',
+ 'bc_php_filter' => 'Using more than 16 filters in a php://filter URL without setting the filter.max_filter_count context option is now deprecated.',
+ 'bc_session_defaults' => 'Sessions are now more secure by default: session.use_strict_mode and session.cookie_httponly are enabled, and session.cookie_samesite is set to Lax.',
+ 'bc_session_handlers' => 'Custom session handlers that do not implement both create_sid() and validateId() are now deprecated.',
+ 'bc_other_deprecations' => 'The metaphone(), mysqli_get_charset(), and mysqli_stmt_init() functions are now deprecated, along with the CSV methods of SplFileObject and the sorting, flag, and serialization methods of ArrayIterator.',
+ 'bc_trim_form_feed' => 'trim(), ltrim(), and rtrim() now also strip form feed (\f) characters by default.',
+ 'bc_stream_close_errors' => 'fclose(), file_put_contents(), and copy() now return false when flushing or closing the stream fails, instead of silently ignoring the failure.',
+ 'bc_value_errors' => 'Many functions now throw a ValueError for out-of-range values, invalid flags, or strings containing NUL bytes, instead of silently truncating or ignoring them.',
+
+ 'footer_title' => 'Better syntax, improved performance and type safety.',
+ 'footer_description' => 'The full list of changes is recorded in the ChangeLog.
Please consult the migration guide for a detailed list of new features and backward-incompatible changes.
', +]; diff --git a/public/releases/8.6/release.inc b/public/releases/8.6/release.inc new file mode 100644 index 0000000000..f75afa9484 --- /dev/null +++ b/public/releases/8.6/release.inc @@ -0,0 +1,555 @@ + version_compare($version, '8.6', '>='), + ); + PHP, + after: <<<'PHP' + $versions = [ + '8.4', + '8.5', + '8.6', + '8.7', + // … + '9.0', + ]; + + $supportsPartialFunctionApplication = array_filter( + $versions, + version_compare(?, '8.6', '>='), + ); + PHP, + ), + new FeatureComparison( + id: 'duration-class', + title: message('duration_title', $lang), + description: message('duration_description', $lang), + links: [ + 'RFC|https://wiki.php.net/rfc/duration_class', + ], + before: '', + after: <<<'PHP' + use Time\Duration; + + $oneSecond = Duration::fromSeconds(1); + $halfSecond = $oneSecond->divideBy(2); + + $onePointFiveSeconds = $oneSecond->add($halfSecond); + + // Sleep for 1.5 seconds + time_nanosleep($onePointFiveSeconds->seconds, $onePointFiveSeconds->nanoseconds); + + $negativeHour = Duration::fromHours(1)->negate(); + + $durations = [ + $oneSecond, + $negativeHour, + $onePointFiveSeconds, + $halfSecond, + ]; + + usort($durations, Duration::compare(...)); // Will sort to [-1h, 0.5s, 1s, 1.5s] + + $baseDelay = Duration::fromMilliseconds(100); + $attempt = 5; + $delay = $baseDelay->multiplyBy(2 ** $attempt); // $delay is equal to 3.2 seconds + PHP, + ), + new FeatureComparison( + id: 'polling-api', + title: message('polling_api_title', $lang), + description: message('polling_api_description', $lang), + links: [ + 'RFC|https://wiki.php.net/rfc/poll_api', + ], + before: <<<'PHP' + $server = stream_socket_server('tcp://0.0.0.0:8080'); + $clients = []; + + while (true) { + $read = [$server, ...$clients]; + $write = $except = null; + + // Based on select(), which typically can't handle more than 1024 connections + stream_select($read, $write, $except, null); + + foreach ($read as $stream) { + if ($stream === $server) { + $clients[] = stream_socket_accept($server); + continue; + } + + $data = fread($stream, 8192); + + if ($data === '' || $data === false) { + unset($clients[array_search($stream, $clients, true)]); + fclose($stream); + } else { + fwrite($stream, $data); + } + } + } + PHP, + after: <<<'PHP' + use Io\Poll\{Context, Event}; + + $server = stream_socket_server('tcp://0.0.0.0:8080'); + + // Automatically uses epoll, kqueue, event ports, or WSAPoll + $poll = new Context(); + $poll->add(new StreamPollHandle($server), [Event::Read]); + + while (true) { + foreach ($poll->wait() as $watcher) { + $stream = $watcher->getHandle()->getStream(); + + if ($stream === $server) { + $client = stream_socket_accept($server); + $poll->add(new StreamPollHandle($client), [Event::Read]); + continue; + } + + $data = fread($stream, 8192); + + if ($data === '' || $data === false) { + $watcher->remove(); + fclose($stream); + } else { + fwrite($stream, $data); + } + } + } + PHP, + ), + new FeatureComparison( + id: 'stream-error-handling', + title: message('stream_error_handling_title', $lang), + description: message('stream_error_handling_description', $lang), + links: [ + 'RFC|https://wiki.php.net/rfc/stream_errors', + ], + before: <<<'PHP' + $config = @file_get_contents('/etc/app/config.json'); + + if ($config === false) { + // Only an unstructured message string is available + $error = error_get_last(); + log_error($error['message']); + } + PHP, + after: <<<'PHP' + $context = stream_context_create([ + 'stream' => [ + 'error_mode' => StreamErrorMode::Exception, + ], + ]); + + try { + $config = file_get_contents('/etc/app/config.json', context: $context); + } catch (StreamException $e) { + foreach ($e->getErrors() as $error) { + // $error->code is a StreamErrorCode enum case + log_error($error->code->name, $error->message, $error->wrapperName); + } + } + PHP, + ), + new FeatureComparison( + id: 'clamp-function', + title: message('clamp_title', $lang), + description: message('clamp_description', $lang), + links: [ + 'RFC|https://wiki.php.net/rfc/clamp_v2', + ], + before: <<<'PHP' + var_dump(max(0, min(-10, 100))); + // int(0) + + var_dump(max(0, min(50, 100))); + // int(50) + + var_dump(max(0, min(110, 100))); + // int(100) + PHP, + after: <<<'PHP' + var_dump(clamp(-10, 0, 100)); + // int(0) + + var_dump(clamp(50, 0, 100)); + // int(50) + + var_dump(clamp(110, 0, 100)); + // int(100) + PHP, + ), + new FeatureComparison( + id: 'sort-direction-enum', + title: message('sort_direction_title', $lang), + description: message('sort_direction_description', $lang), + links: [ + 'RFC|https://wiki.php.net/rfc/sort_direction_enum', + ], + before: <<<'PHP' + class QueryBuilder + { + private int $sort; + + public function setSort(int $sort): void + { + $this->sort = $sort; + } + + private function getSortKeyword(): string + { + return match($this->sort) { + SORT_ASC => 'ASC', + SORT_DESC => 'DESC', + }; + } + } + PHP, + after: <<<'PHP' + class QueryBuilder + { + private SortDirection $sort; + + public function setSort(SortDirection $sort): void + { + $this->sort = $sort; + } + + private function getSortKeyword(): string + { + return match($this->sort) { + SortDirection::Ascending => 'ASC', + SortDirection::Descending => 'DESC', + }; + } + } + PHP, + ), + new FeatureComparison( + id: 'doc-comments', + title: message('doc_comments_title', $lang), + description: message('doc_comments_description', $lang), + links: [ + 'RFC|https://wiki.php.net/rfc/parameter-doccomments', + ], + before: <<<'PHP' + class DB + { + /** + * Returns up to $limit items matching the $query. + * + * @param string $query Terms to search + * @param int $limit Maximum number of results + */ + function search(string $query, int $limit = 10): array { + return []; + } + } + + // Parameter descriptions can only be extracted + // by parsing the function's DocComment. + PHP, + after: <<<'PHP' + class DB + { + /** + * Returns up to $limit items matching the $query. + */ + function search( + /** Terms to search */ + string $query, + + /** Maximum number of results */ + int $limit = 10, + ): array { + return []; + } + } + + var_dump( + new ReflectionParameter([DB::class, 'search'], 'query')->getDocComment() + ); + // string(22) "/** Terms to search */" + PHP, + ), + new FeatureComparison( + id: 'isreadable-iswriteable', + title: message('isreadable_iswriteable_title', $lang), + description: message('isreadable_iswriteable_description', $lang), + links: [ + 'RFC|https://wiki.php.net/rfc/isreadable-iswriteable', + ], + before: '', + after: <<<'PHP' + class NewsEntry + { + public string $title = 'PHP 8.6 released!'; + + public private(set) string $slug = 'php-86-released'; + } + + $entry = new NewsEntry(); + foreach (['title', 'slug'] as $item) { + $property = new ReflectionProperty(NewsEntry::class, $item); + printf( + "%s: isReadable=%s isWritable=%s\n", + $item, + $property->isReadable(null, $entry) ? 'yes' : 'no', + $property->isWritable(null, $entry) ? 'yes' : 'no' + ); + } + // title: isReadable=yes isWritable=yes + // slug: isReadable=yes isWritable=no + PHP, + ), + new FeatureComparison( + id: 'readonly-property-defaults', + title: message('readonly_property_defaults_title', $lang), + description: message('readonly_property_defaults_description', $lang), + links: [ + 'RFC|https://wiki.php.net/rfc/readonly_property_defaults', + ], + before: <<<'PHP' + interface Rule + { + public string $parser { get; } + } + + final readonly class SlugRule implements Rule + { + public string $parser; + + public function __construct() + { + $this->parser = SlugParser::class; + } + } + PHP, + after: <<<'PHP' + interface Rule + { + public string $parser { get; } + } + + final readonly class SlugRule implements Rule + { + public string $parser = SlugParser::class; + } + PHP, + ), +]; + +echo ReleasePage::getHeroSection( + title: message('main_title', $lang), + subtitle: message('main_subtitle', $lang), + logoSvg: <<<'SVG' + + SVG, + upgradeNow: message('upgrade_now', $lang), + whatsNew: message('whats_new', $lang), + released: message('released', $lang), +); +?> + +