diff --git a/public/images/php8/logo_php8_6.svg b/public/images/php8/logo_php8_6.svg new file mode 100644 index 0000000000..cab6a6b6fe --- /dev/null +++ b/public/images/php8/logo_php8_6.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/php8/php_8_6_released.png b/public/images/php8/php_8_6_released.png new file mode 100644 index 0000000000..fe141d4be4 Binary files /dev/null and b/public/images/php8/php_8_6_released.png differ diff --git a/public/releases/8.6/common.php b/public/releases/8.6/common.php new file mode 100644 index 0000000000..a30f617e19 --- /dev/null +++ b/public/releases/8.6/common.php @@ -0,0 +1,65 @@ + 'English', +]; + +function common_header(string $description): void { + global $MYSITE, $lang; + + $meta_image_url = \filter_var($MYSITE . 'images/php8/php_8_6_released.png', \FILTER_VALIDATE_URL); + $meta_image_path = $meta_image_url === false ? '' : \htmlspecialchars($meta_image_url); + $meta_description = \htmlspecialchars($description); + + $languages = []; + foreach (LANGUAGES as $code => $text) { + $languages[] = ['name' => $text, 'selected' => $lang === $code, 'url' => '/releases/8.6/' . $code . '.php']; + } + + \site_header("PHP 8.6 Release Announcement", [ + 'current' => 'php85', + 'css' => ['php85.css'], + 'language_switcher' => $languages, + 'theme_switcher' => true, + 'meta_tags' => << + + + + + + + + + + + + +META + ]); +} + +/** + * @param array $interpolations + */ +function message(string $code, string $language = 'en', array $interpolations = []): string +{ + $original = require __DIR__ . '/languages/en.php'; + if (($language !== 'en') && file_exists(__DIR__ . '/languages/' . $language . '.php')) { + $translation = require __DIR__ . '/languages/' . $language . '.php'; + } + + $message = $translation[$code] ?? $original[$code] ?? $code; + + foreach ($interpolations as $name => $value) { + $message = str_replace("{{$name}}", $value, $message); + } + + return $message; +} diff --git a/public/releases/8.6/en.php b/public/releases/8.6/en.php new file mode 100644 index 0000000000..569ea7b5d0 --- /dev/null +++ b/public/releases/8.6/en.php @@ -0,0 +1,5 @@ +chooseCode("", "", $_SERVER['HTTP_ACCEPT_LANGUAGE']); + +mirror_redirect("/releases/8.6/$lang.php"); diff --git a/public/releases/8.6/languages/en.php b/public/releases/8.6/languages/en.php new file mode 100644 index 0000000000..0d538cee16 --- /dev/null +++ b/public/releases/8.6/languages/en.php @@ -0,0 +1,78 @@ + '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', + 'main_title' => 'Smarter, Faster, Built for Tomorrow.', + 'main_subtitle' => '

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 new Io\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 a StreamException 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), +); +?> + +
+ + +
+

+ +
+ +
+
+
+
+
+ + + + + + +
+
+
+
+
+
+ + + + + + + +
+
+
+
+
+
+ + + + + + +
+
+
+
+
+
+ + + + + + + +
+
+
+
+
+
+ + + + + + + +
+
+
+
+
+
+ + + + + + + + +
+
+
+
+
+
+ + + +
+
+
+
+

+
    +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
+
+
+

+
    +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
+
+
+
+
+ false]); diff --git a/public/styles/php85.css b/public/styles/php85.css index c6cc01da9e..23aa6228e6 100644 --- a/public/styles/php85.css +++ b/public/styles/php85.css @@ -442,7 +442,8 @@ a { background-image: radial-gradient(at 50% 50%, var(--color-white), transparent 65%); padding-inline: calc(var(--spacing) * 4); text-align: center; - margin-inline: auto + margin-inline: auto; + width: 100%; } @media (min-width: 40rem) { @@ -569,7 +570,8 @@ a { margin-inline: auto; margin-bottom: calc(var(--spacing) * 6); height: calc(var(--spacing) * 12); - color: var(--color-gray-800) + color: var(--color-gray-800); + width: auto; } @media (min-width: 64rem) { @@ -1073,7 +1075,7 @@ a { } .before-and-after-title-and-description h2 { - font-size: var(--text-2xl); + font-size: var(--text-xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); font-weight: var(--font-weight-bold) }