diff --git a/.docker/Dockerfile b/.docker/Dockerfile new file mode 100644 index 0000000000..ccb97e4fe5 --- /dev/null +++ b/.docker/Dockerfile @@ -0,0 +1,18 @@ +FROM php:8.5-cli + +ARG DEBIAN_FRONTEND=noninteractive + +# 2. Install system dependencies and Node.js/npm +# We are using Node.js 22 LTS as a reliable, modern standard +RUN apt-get update && apt-get install -y \ + curl gnupg \ + && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y nodejs \ + && apt-get clean \ + && rm -rf /var/lib/lists/* + +# 3. Globally install TypeScript and the Socket Dev CLI +RUN npm install -g typescript socket tsx esbuild + +# 4. Set up a default working directory (Optional) +WORKDIR /app diff --git a/.router.php b/.router.php index c204ce635e..98ce5a1773 100644 --- a/.router.php +++ b/.router.php @@ -4,12 +4,19 @@ $filename = $_SERVER["PATH_INFO"] ?? $_SERVER["SCRIPT_NAME"]; +//die(print_r($_SERVER, true)); + +//$_SERVER['HTTP_HOST'] = ''; +//$_SERVER['BASE_PAGE'] = '/'; +//$_SERVER['SERVER_NAME'] = 'localhost'; + if (!file_exists($_SERVER["DOCUMENT_ROOT"] . $filename)) { require_once __DIR__ . '/error.php'; return; } + /* This could be an image or whatever, so don't try to compress it */ ini_set("zlib.output_compression", 0); return false; diff --git a/bin/createNewsEntry b/bin/createNewsEntry index 937330218a..f4d8868558 100755 --- a/bin/createNewsEntry +++ b/bin/createNewsEntry @@ -180,6 +180,8 @@ function parseOptions(): Entry { echo " --conf-time 'value' When the event will be occurign (cfp and conference categories only)\n"; echo " --content 'value' Text content for the entry, may include XHTML\n"; echo " --content-file 'value' Name of file to load content from, may not be specified with --content\n"; + echo " --summary 'value' Short, plain-text summary of the content. Should be a single line.\n"; + echo " --summary-file 'value' Name of the file to load summary content from, should be a plain-text summary of the content. Should be a single line.\n"; echo " --image-path 'value' Basename of image file in " . Entry::IMAGE_PATH_REL . "\n"; echo " --image-title 'value' Title for the image provided\n"; echo " --image-link 'value' URI to direct to when clicking the image\n"; @@ -238,5 +240,22 @@ function parseOptions(): Entry { exit(1); } + $summary = $opts['summary'] ?? ''; + $summaryFile = $opts['summary-file'] ?? ''; + if ($summary && $summaryFile) { + fwrite(STDERR, "--summary and --summary-file may not be specified together\n"); + exit(1); + } elseif ($summaryFile) { + $summary = file_get_contents($summaryFile); + if ($summary === false) { + fwrite(STDERR, "Summary cannot be opened, or must not be empty\n"); + exit(1); + } + } + + if ($summary) { + $entry->setSummary($summary); + } + return $entry; } diff --git a/docs.php b/docs.php index a7003afd07..c531c7a159 100644 --- a/docs.php +++ b/docs.php @@ -7,54 +7,46 @@ site_header("Documentation", ["current" => "docs"]); +$lang = Languages::ACTIVE_ONLINE_LANGUAGES_EX['en']; ?>

Documentation

-
- -

- The PHP Manual is available online in a selection of languages. - Please pick a language from the list below. -

- -

- Note, that many languages are just under translation, and - the untranslated parts are still in English. Also some translated - parts might be outdated. The translation teams are - open to contributions. -

+

PHP Translates its documentation into many different languages, but the main reference is in English

+
+ Flag of <?= htmlspecialchars($lang['label_loc'])?> +
+
+
+
+
-

- View Online: - $langname) { - if (!file_exists($_SERVER["DOCUMENT_ROOT"] . "/manual/{$langcode}/index.php")) { - continue; - } - // Make preferred language bold - if ($langcode === $LANG) { echo ""; } - - echo '' . $langname . ''; - echo ($lastlang !== $langcode) ? ",\n" : "\n"; - - if ($langcode === $LANG) { echo ""; } -} +

Available Languages

+
+

+ Note, that many languages are just under translation, and + the untranslated parts are still in English. Also some translated + parts might be outdated. The translation teams are + open to contributions. +

+
+ $lang) { ?> +
+ Flag of <?= htmlspecialchars($lang['label_loc'])?> +
+
+
+
+
+ +
+

-?> -

+
-

- The language currently being used as the default for you should be in - bold above. You can change the setting for this on the - My PHP.net customization page. -

-

+

More Options

For downloadable formats, please visit our documentation downloads page.

diff --git a/images/landing/contribute.png b/images/landing/contribute.png new file mode 100644 index 0000000000..07d570ab2c Binary files /dev/null and b/images/landing/contribute.png differ diff --git a/images/landing/docs-translator.png b/images/landing/docs-translator.png new file mode 100644 index 0000000000..1f28200e8f Binary files /dev/null and b/images/landing/docs-translator.png differ diff --git a/images/landing/externals-io.svg b/images/landing/externals-io.svg new file mode 100644 index 0000000000..b1480073ef --- /dev/null +++ b/images/landing/externals-io.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/landing/php-bugs.png b/images/landing/php-bugs.png new file mode 100644 index 0000000000..e3f3004315 Binary files /dev/null and b/images/landing/php-bugs.png differ diff --git a/images/language-flags/br.png b/images/language-flags/br.png new file mode 100644 index 0000000000..eb88391c1a Binary files /dev/null and b/images/language-flags/br.png differ diff --git a/images/language-flags/de.png b/images/language-flags/de.png new file mode 100644 index 0000000000..0a62147326 Binary files /dev/null and b/images/language-flags/de.png differ diff --git a/images/language-flags/en.webp b/images/language-flags/en.webp new file mode 100644 index 0000000000..833789753e Binary files /dev/null and b/images/language-flags/en.webp differ diff --git a/images/language-flags/es.png b/images/language-flags/es.png new file mode 100644 index 0000000000..ce714320f5 Binary files /dev/null and b/images/language-flags/es.png differ diff --git a/images/language-flags/fr.png b/images/language-flags/fr.png new file mode 100644 index 0000000000..94a8acbec1 Binary files /dev/null and b/images/language-flags/fr.png differ diff --git a/images/language-flags/it.png b/images/language-flags/it.png new file mode 100644 index 0000000000..66568e44fa Binary files /dev/null and b/images/language-flags/it.png differ diff --git a/images/language-flags/ja.png b/images/language-flags/ja.png new file mode 100644 index 0000000000..f009df369b Binary files /dev/null and b/images/language-flags/ja.png differ diff --git a/images/language-flags/ru.png b/images/language-flags/ru.png new file mode 100644 index 0000000000..fdd45a9b75 Binary files /dev/null and b/images/language-flags/ru.png differ diff --git a/images/language-flags/tr.png b/images/language-flags/tr.png new file mode 100644 index 0000000000..3510020b54 Binary files /dev/null and b/images/language-flags/tr.png differ diff --git a/images/language-flags/uk.webp b/images/language-flags/uk.webp new file mode 100644 index 0000000000..16abefbd4e Binary files /dev/null and b/images/language-flags/uk.webp differ diff --git a/images/language-flags/zh.webp b/images/language-flags/zh.webp new file mode 100644 index 0000000000..fb0a0d1d30 Binary files /dev/null and b/images/language-flags/zh.webp differ diff --git a/images/logos/composer.png b/images/logos/composer.png new file mode 100644 index 0000000000..e0782d0593 Binary files /dev/null and b/images/logos/composer.png differ diff --git a/images/logos/github_invertocat_white.svg b/images/logos/github_invertocat_white.svg new file mode 100644 index 0000000000..527ba11c50 --- /dev/null +++ b/images/logos/github_invertocat_white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/images/logos/php-foundation.svg b/images/logos/php-foundation.svg new file mode 100644 index 0000000000..cf5b3a6942 --- /dev/null +++ b/images/logos/php-foundation.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/images/logos/phpc-discord.png b/images/logos/phpc-discord.png new file mode 100644 index 0000000000..7a79320cc7 Binary files /dev/null and b/images/logos/phpc-discord.png differ diff --git a/images/logos/reddit.png b/images/logos/reddit.png new file mode 100644 index 0000000000..9ce7ce2541 Binary files /dev/null and b/images/logos/reddit.png differ diff --git a/include/branch-meta.inc b/include/branch-meta.inc new file mode 100644 index 0000000000..c68e1eae25 --- /dev/null +++ b/include/branch-meta.inc @@ -0,0 +1,92 @@ + [ + 'support_label' => 'Supported', + 'features' => [ + [ + 'title' => 'URI Extension', + 'about' => 'PHP 8.5 adds a built-in URI extension to parse, normalize, and handle URLs following RFC 3986 and WHATWG URL standards.', + ], + [ + 'title' => 'Pipe Operator', + 'about' => 'The |> operator enables chaining callables left-to-right, passing values smoothly through multiple functions without intermediary variables.', + ], + [ + 'title' => 'Clone With', + 'about' => 'Clone objects and update properties with the new clone() syntax, making the "with-er" pattern simple for readonly classes.', + ], + [ + 'title' => '#[\NoDiscard] Attribute', + 'about' => 'The #[\NoDiscard] attribute warns when a return value isn’t used, helping prevent mistakes and improving overall API safety.', + ], + [ + 'title' => 'Closures and First-Class Callables in Constant Expressions', + 'about' => 'Static closures and first-class callables can now be used in constant expressions, such as attribute parameters.', + ], + [ + 'title' => 'Persistent cURL Share Handles', + 'about' => 'Handles can now be persisted across multiple PHP requests, avoiding the cost of repeated connection initialization to the same hosts.', + ], + ], + ], + '8.4' => [ + 'support_label' => 'Supported', + 'features' => [ + [ + 'title' => 'Property Hooks', + 'short' => 'Property Hooks allow intercepting properties', + ], + [ + 'title' => 'Asymmetric Property Visibility', + 'short' => 'Asymmetric Visibility for get and set', + ], + [ + 'title' => '#[Derpeciated] Attribute', + 'short' => '#[Depreciated] attribute signals removal intent', + ], + [ + 'title' => 'Additional Array Functions', + 'short' => 'New array lookup and query options', + ], + ], + ], + '8.3' => [ + 'support_label' => 'Security Support', + 'features' => [ + [ + 'title' => 'Typed Class Constants', + 'short' => 'Class constants can now be typed', + ], + [ + 'title' => 'Dynamic Class Constants', + 'about' => 'Class constants can now be accessed via dynamic calls', + ], + [ + 'title' => 'Readonly Deep Cloning', + 'short' => 'Enhanced deep cloning of readonly instances', + ], + [ + 'title' => 'Randomizer Improvements', + 'short' => 'Generate random strings from provided character sets', + ], + ], + ], + '8.2' => [ + 'support_label' => 'Security Support', + 'features' => [ + [ + 'title' => 'Readonly classes', + 'short' => 'Entire classes can now be marked Readonly', + ], + [ + 'title' => 'Disjunction Normal Form Types', + 'short' => 'Improved type support with Disjunction Normal Forms', + ], + [ + 'title' => 'Improved Standalone Types', + 'short' => 'Null, true and false are now usable as types', + ], + ], + ], +]; diff --git a/include/branches.inc b/include/branches.inc index deb1f79841..bf56899bc4 100644 --- a/include/branches.inc +++ b/include/branches.inc @@ -89,6 +89,11 @@ function format_interval($from, DateTime $to) { return $eolPeriod; } +function get_distribution_base_url(): string +{ + return 'https://www.php.net/distributions'; +} + function version_number_to_branch(string $version): ?string { $parts = explode('.', $version); if (count($parts) > 1) { @@ -138,9 +143,14 @@ function get_all_branches() { function get_active_branches($include_recent_eols = true) { $branches = []; $now = new DateTime(); + $baseUrl = get_distribution_base_url(); foreach ($GLOBALS['RELEASES'] as $major => $releases) { foreach ($releases as $version => $release) { + foreach ($release['source'] as $sourceId => $source) { + $releases[$version]['source'][$sourceId]['url'] = $baseUrl . $source['filename']; + } + $branch = version_number_to_branch($version); if ($branch) { diff --git a/include/communities.inc b/include/communities.inc new file mode 100644 index 0000000000..5b1dc7d5bd --- /dev/null +++ b/include/communities.inc @@ -0,0 +1,22 @@ + 'Reddit', + 'about' => 'Reddit has an active PHP community discussing the language and its ecosystem.', + 'image' => '/images/logos/reddit.png', + 'href' => 'https://www.reddit.com/r/PHP/', + ], + [ + 'title' => 'PHP Community Discord', + 'about' => 'Join thousands of users on Discord talking about PHP.', + 'image' => '/images/logos/phpc-discord.png', + 'href' => 'https://discord.phpc.social/', + ], + [ + 'title' => 'Official Mailing Lists', + 'about' => 'Help and guidance, as well as proposals & discussions on the future of the language.', + 'image' => '/images/logos/new-php-logo.png', + 'href' => '/mailing-lists.php', + ], +]; diff --git a/include/development-links.inc b/include/development-links.inc new file mode 100644 index 0000000000..91f36db729 --- /dev/null +++ b/include/development-links.inc @@ -0,0 +1,46 @@ + 'PHP on Github', + 'about' => 'Browse and contribute to the source code behind the PHP engine and infrastructure.', + 'image' => '/images/logos/github_invertocat_white.svg', + 'href' => 'https://github.com/php', + 'href_label' => 'Visit GitHub', + ], + [ + 'title' => 'RFCs / Language Proposals', + 'about' => 'Requests for Comments are the mechanism PHP internals uses to propose language changes.', + 'image' => 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/PHP-logo.svg/500px-PHP-logo.svg.png', + 'href' => 'https://wiki.php.net/rfc', + 'href_label' => 'View Proposals', + ], + [ + 'title' => 'PHP Internals (externals.io)', + 'about' => 'Browse discussions from PHP Internals about current and future enhancements.', + 'image' => '/images/landing/externals-io.svg', + 'href' => 'https://externals.io', + 'href_label' => 'Browser Externals', + ], + [ + 'title' => 'Get Involved', + 'about' => 'Find ways to contribute to the PHP engine, documentation and more.', + 'image' => '/images/landing/contribute.png', + 'href' => '/get-involved.php', + 'href_label' => 'Get Involved', + ], + [ + 'title' => 'Submit a Bug Report', + 'about' => 'Found a bug in the PHP runtime? Help us out by submitting it to our issue tracker.', + 'image' => '/images/landing/php-bugs.png', + 'href' => 'https://github.com/php/php-src/issues', + 'href_label' => 'Browse & Submit Issues', + ], + [ + 'title' => 'Documentation Translation', + 'about' => 'Help our team translate our documentation into multiple languages.', + 'image' => '/images/landing/docs-translator.png', + 'href' => 'https://doc.php.net/guide/', + 'href_label' => 'Learn About Translation', + ] +]; diff --git a/include/footer.inc b/include/footer.inc index faa7305d68..7ddcb50a4f 100644 --- a/include/footer.inc +++ b/include/footer.inc @@ -1,4 +1,7 @@ - + + + + "; diff --git a/include/header.inc b/include/header.inc index 48722edafa..92156b4d59 100644 --- a/include/header.inc +++ b/include/header.inc @@ -62,7 +62,7 @@ if (!isset($config["languages"])) { - "> + <?php echo $title ?> @@ -106,7 +106,7 @@ if (!isset($config["languages"])) { - "> + @@ -357,6 +357,13 @@ if (!isset($config["languages"])) { + + + + +
@@ -366,5 +373,7 @@ if (!isset($config["languages"])) { +
+ diff --git a/include/landing-heros.inc b/include/landing-heros.inc new file mode 100644 index 0000000000..ed84fb7d3c --- /dev/null +++ b/include/landing-heros.inc @@ -0,0 +1,46 @@ + 'try-it-now', + 'title' => 'Try It Now', + 'about' => 'Begin writing PHP code immediately within a browser-based sandbox. No install required.', + 'href' => '/sandbox/sandbox.php', + 'href_label' => 'Launch Sandbox', + ], + [ + 'id' => 'why-use-php', + 'title' => 'Why Use PHP?', + 'about' => 'Learn why PHP powers the global web - from individual blogs to enormous enterprises.', + 'href' => 'https://web-php-pr-1172.preview.thephp.foundation/why-use-php', + 'href_label' => 'Discover Why', + ], + [ + 'id' => 'learn', + 'title' => 'Learn', + 'about' => 'Browse the documentation, including extensive tutorials and guidance.', + 'href' => '/docs', + 'href_label' => 'Browse Docs', + ], + [ + 'id' => 'releases', + 'title' => 'Releases', + 'about' => 'View currently supported PHP runtimes including download logs and highlight features.', + 'href' => '#releases', + 'href_label' => 'View Runtimes', + ], + [ + 'id' => 'community', + 'title' => 'Community', + 'about' => 'Get involved with the PHP Community via forums, live chat and conferences.', + 'href' => '#community', + 'href_label' => 'Get Engaged', + ], + [ + 'id' => 'language-development', + 'title' => 'Language Development', + 'about' => 'See how the PHP language works to evolve, and maybe even get involved yourself!', + 'href' => '#language-development', + 'href_label' => 'Find Out More', + ], +]; diff --git a/include/layout.inc b/include/layout.inc index b9cc7c0b63..0455508881 100644 --- a/include/layout.inc +++ b/include/layout.inc @@ -495,7 +495,6 @@ META if (isset($_SERVER['BASE_PAGE']) && $shortname = get_shortname($_SERVER["BASE_PAGE"])) { $shorturl = "https://www.php.net/" . $shortname; } - require __DIR__ . "/header.inc"; } function site_footer(array $config = []): void diff --git a/include/prepend.inc b/include/prepend.inc index b2f3969f11..6409202756 100644 --- a/include/prepend.inc +++ b/include/prepend.inc @@ -121,3 +121,8 @@ function google_cse(): void { echo $cse_snippet; } + +function safe(string $html): string +{ + return htmlspecialchars($html, encoding: 'UTF-8'); +} diff --git a/js/common.js b/js/common.js index 86b4862fd5..c00fc103eb 100644 --- a/js/common.js +++ b/js/common.js @@ -879,3 +879,23 @@ function applyTheme(theme) { } applyTheme(savedTheme) + +function shuffleImmutableArray(array) { + const newArray = [...array]; + for (let i = newArray.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; + } + return newArray; +} + +function shuffleDOMChildrenWithLimit(parent, limit) { + const children = Array.from(parent.children); + const replacements = shuffleImmutableArray(children.slice(0, limit)); + + while (parent.children.length) { + parent.removeChild(parent.children[0]); + } + + replacements.forEach(n => parent.appendChild(n)); +} diff --git a/js/sandbox.js b/js/sandbox.js new file mode 100644 index 0000000000..5c9768426a --- /dev/null +++ b/js/sandbox.js @@ -0,0 +1,41 @@ +import phpBinary from "./php-web.mjs"; + +export class PHPSandbox { + constructor(templateFiles) { + this.templateFiles = templateFiles; + } + + async execute(files) { + let buffer = []; + let initializing = true; + + files = {...files, ...this.templateFiles}; + + const php = await phpBinary({ + print(data) { + if (initializing) { + return; + } + + console.log('output', data); + + buffer.push(data); + } + }); + + for (const [filename, content] of Object.entries(files)) { + const dir = filename.substring(0, filename.lastIndexOf('/')); + if (dir) { + php.FS_createPath('/', dir, true, true); + } + + php.FS.writeFile('/' + filename, content); + } + + initializing = false; + php.ccall("phpw_run", null, ["string"], ['require "boot.php";']); + + return JSON.parse(buffer.join("")); + } +} + diff --git a/landing.php b/landing.php new file mode 100644 index 0000000000..1cff587b13 --- /dev/null +++ b/landing.php @@ -0,0 +1,483 @@ + '/language.operators.comparison#language.operators.comparison.ternary', + '/??' => '/language.operators.comparison#language.operators.comparison.coalesce', + '/??=' => '/language.operators.assignment#language.operators.assignment.other', + ]; + if (isset($shortcuts[$uri])) { + header("Location: {$shortcuts[$uri]}"); + exit; + } +})($_SERVER['REQUEST_URI'] ?? ''); + +// Get the modification date of this PHP file +$timestamps = [@getlastmod()]; + +/* + The date of prepend.inc represents the age of ALL + included files. Please touch it if you modify any + other include file (and the modification affects + the display of the index page). The cost of stat'ing + them all is prohibitive. +*/ +$timestamps[] = @filemtime("include/prepend.inc"); + +// These are the only dynamic parts of the frontpage +$timestamps[] = @filemtime("include/pregen-confs.inc"); +$timestamps[] = @filemtime("include/pregen-news.inc"); +$timestamps[] = @filemtime("include/version.inc"); +$timestamps[] = @filemtime("js/common.js"); + +// The latest of these modification dates is our real Last-Modified date +$timestamp = max($timestamps); + +// Note that this is not a RFC 822 date (the tz is always GMT) +$tsstring = gmdate("D, d M Y H:i:s ", $timestamp) . "GMT"; + +// Check if the client has the same page cached +if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && + ($_SERVER["HTTP_IF_MODIFIED_SINCE"] == $tsstring)) { + header("HTTP/1.1 304 Not Modified"); + exit(); +} + +// Inform the user agent what is our last modification date +header("Last-Modified: " . $tsstring); + +$_SERVER['BASE_PAGE'] = 'index.php'; +include_once 'include/prepend.inc'; +include_once 'include/branches.inc'; +include_once 'include/pregen-confs.inc'; +include_once 'include/version.inc'; + +mirror_setcookie("LAST_NEWS", $_SERVER["REQUEST_TIME"], 60 * 60 * 24 * 365); + +$active_branches = get_active_branches(); +$active_branches_sorted = []; + +/** @var array}> $branch_descriptors */ +$branch_descriptors = require __DIR__ . '/include/branch-meta.inc'; + +krsort($active_branches); +foreach ($active_branches as $major => $releases) { + ksort($releases); + $releases = array_reverse($releases); + + foreach ($releases as $release) { + $version = $release['version']; + [$major, $minor, $_] = explode('.', $version); + $versionLabel = $major . '.' . $minor; + $branch = $branch_descriptors[$versionLabel] ?? []; + + $active_branches_sorted[] = [ + ...$release, + 'major' => $major, + 'minor' => $minor, + 'version_ex' => $major . '.'. $minor, + 'label' => $versionLabel, + 'download_url' => '/downloads.php?version=' . $versionLabel, + 'more_url' => '/releases/' . $versionLabel . '/en.php', + 'changelog_url' => '/ChangeLog-' . $major . '.php#' . $version, + 'migration_url' => '/migration' . $major . $minor, + 'security_eol' => get_branch_security_eol_date($versionLabel), + 'support_eol' => get_branch_bug_eol_date($versionLabel), + 'meta' => $branch, + 'logo' => '/images/php8/logo_php' . $major . '_' . $minor . '.svg', + 'features' => [ + ...($branch['features'] ?? []), + ] + ]; + } +} + +$latest = array_shift($active_branches_sorted); + +function buildNavCard(NavCardItem $card, array $config = []): string +{ + $config = [ + 'cn_card' => 'landing-cc-card', + 'cn_card_content' => 'landing-cc-card-content', + 'cn_card_img' => 'landing-cc-card-img', + ...$config, + ]; + + ob_start(); + ?> + id) { ?>id="id) ?>" href="href) ?>" class="vgrid-card landing-card-ovh "> +
+
+ image) { ?>Graphic of <?= $card->title ?> +
title) ?>
+
+ +
about) ?>
+ + href) { ?> +
+ href_label)?> +
+ +
+
+ buildNavCard($card, $config), $cards)); +} + +function drawBranchInfo(array $release): void +{ + $now = new DateTime(); + $fmtDate = fn(DateTime $date) => ($date < $now) ? 'End of Life' : $date->format('Y-m-d'); + + ?> +
+
+ · + Changelog · Upgrading +
+
+
+
+ Bugfixes: + +
+
+ Security: + +
+
+
+
+ getConferences() as $event) { + $id = parse_url($event["id"], PHP_URL_FRAGMENT); + $image = $event["newsImage"]['content'] ?? null; + if ($image) { + $image = '/images/news/' . $image; + } + + $eventCards[] = new NavCardItem( + title: $event['title'], + about: $event['summary'] ?? '', + image: $image, + href: '/events', + href_label: 'Explore Event', + ); + + if (count($eventCards) > 12) { + break; + } +} + +$developmentCards = []; +foreach (require __DIR__ . '/include/development-links.inc' as $community) { + $developmentCards[] = new NavCardItem( + title: $community['title'], + about: $community['about'], + image: $community['image'], + href: $community['href'], + href_label: $community['href_label'] ?? 'Visit Community', + ); +} + +$heroCards = []; +foreach (require __DIR__ . '/include/landing-heros.inc' as $hero) { + $heroCards[] = new NavCardItem( + title: $hero['title'], + about: $hero['about'], + image: $entry["newsImage"]["content"] ?? '', + href: $hero['href'], + href_label: $hero['href_label'], + id: isset($hero['id']) ? ('hero-' . $hero['id']) : null, + ); +} + + +ob_start(); +?> + +
+
+
+ +

Powering Solo Developers, Teams, and Global Enterprise

+
+
+
+
Fast & Modern
+
PHP provides blistering fast performance and a modern developer-focused experience.
+
+ +
+
A Massive Ecosystem
+
Leverage over 300,000 existing open source packages for your projects, along with powerful tooling.
+
+ +
+
An Established Community
+
Millions of developers and businesses already use PHP to achieve their goals every day.
+
+
+
+
+ +
+ +
+ + +
+
+
+
+ + + +
+
+
+ + + + + + + + + + + +
+
+
+ php +
+ +
+ + +
+
+
Major Features
+
+ +
+
+
+
+ +
+
+
+
+
+ + +
+
+ +
+
+
+ PHP Foundation Logo +
+
+ The PHP Foundation is a collective of people and organizations, united in the mission to ensure the long-term prosperity of the PHP language. +

+
+ + Learn About the PHP Foundation +  ·  + + Donate Via Open Collective +  ·  + + Donate Via GitHub +
+
+
+
+ + +
+
+
+
Community
+
+
+
+
+ +
Events & Conferences
+
+
+
+
+
+
+ +
+
+
+ PHP Foundation Logo +
+
+ PHP has one of the largest collections of open-source libraries in the world. + +

+ Ranging from individual helpers to entire application frameworks, all packages are easily installable via the Composer + package manager. +

+
+ + Get Composer +  ·  + Browse Package Repository +
+
+
+
+ +
+
+
Language Development
+
+ 'landing-cc-card landing-cc-card-grey']) ?> +
+
+
+ + + + 'home', + 'headtags' => [ + '', + '', + ], + 'link' => [ + [ + "rel" => "search", + "type" => "application/opensearchdescription+xml", + "href" => $MYSITE . "phpnetimprovedsearch.src", + "title" => "Add PHP.net search", + ], + [ + "rel" => "alternate", + "type" => "application/atom+xml", + "href" => $MYSITE . "releases/feed.php", + "title" => "PHP Release feed", + ], + + ], + 'css' => ['home.css'], + 'include_section' => false, + ], +); + +echo $header; + +// Print the common footer. +site_footer([ + 'include_section' => false, + "atom" => "/feed.atom", // Add a link to the feed at the bottom +]); +} catch (Throwable $e) { + http_response_code(500); + die((string)$e); +} diff --git a/sandbox/boot.inc b/sandbox/boot.inc new file mode 100644 index 0000000000..5a17b78e52 --- /dev/null +++ b/sandbox/boot.inc @@ -0,0 +1,112 @@ + */ + public array $error_logs = []; + + public static function shared(): self + { + return self::$gs ??= new self(); + } + + public function __construct() + { + set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) { + $this->error_logs[] = [ + 'errno' => $errno, + 'message' => $errstr, + 'file' => $errfile, + 'line' => $errline, + ]; + }); + } + + public function formatException(Throwable $e): array + { + $stack = [[ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]]; + + foreach ($e->getTrace() as $trace) { + $where = []; + + if (isset($trace['class'])) { + $where[] = $trace['class']; + } + + if (isset($trace['function'])) { + $where[] = $trace['function']; + } + + + $stack[] = [ + 'file' => $trace['file'], + 'line' => (int)$trace['line'], + ]; + + $stack[count($stack) - 2]['where'] = implode('->', $where); + } + + /** @var array> $fileLineCache */ + $fileLineCache = []; + foreach ($stack as $idx => $item) { + if (!$item['file'] || !file_exists($item['file'])) { + $stack[$idx]['snippet'] = null; + continue; + } + + $lines = $fileLineCache[$item['file']] ??= explode("\n", file_get_contents($item['file'])); + $stack[$idx]['snippet'] = trim($lines[(int)$item['line'] - 1] ?? ''); + } + + return [ + 'message' => $e->getMessage(), + 'stack' => $stack, + 'previous' => $e->getPrevious() ? $this->formatException($e->getPrevious()) : null, + ]; + } + + public function run() + { + $result = [ + 'response_type' => 'text/plain', + ]; + + $this->error_logs = []; + $output = null; + $result = null; + $path = 'success'; + + try { + ob_start(); + require "./entry.php"; + $result['mode'] = 'success'; + $result['buffer'] = ob_get_clean(); + } catch (Throwable $e) { + $result['mode'] = 'success'; + $result['exception'] = $this->formatException($e); + $result['buffer'] = (string)$e; + ob_get_clean(); + } + + $result['errors'] = $this->error_logs; + + echo json_encode($result); + } +} + +GlobalSandbox::shared()->run(); + + + diff --git a/sandbox/example.txt b/sandbox/example.txt new file mode 100644 index 0000000000..07d684c257 --- /dev/null +++ b/sandbox/example.txt @@ -0,0 +1,37 @@ + + +
+
PHP Sandbox
+
+
+
+
Editor
+
+ +
+
+ +
+
+
+
Output
+
+
+
+
+
An Exception Occurred
+
+
+ +
+
Stack Trace
+
The stack trace shows the path the code took before it encountered the error. The last code to execute is at the top.
+ +
+
+
+
+ +
+
+
+
+
+
+
Debug
+
+
+
+ + +
+
+
+
+
+
+
+
+ + + + 'Chinese (Simplified)', ]; + public const ACTIVE_ONLINE_LANGUAGES_EX = [ + 'en' => [ + 'label_en' => 'English', + 'label_loc' => 'English', + 'icon' => '/images/language-flags/en.webp', + ], + 'de' => [ + 'label_en' => 'German', + 'label_loc' => 'Deutsch', + 'icon' => '/images/language-flags/de.png', + ], + 'es' => [ + 'label_en' => 'Spanish', + 'label_loc' => 'Español', + 'icon' => '/images/language-flags/es.png', + ], + 'fr' => [ + 'label_en' => 'French', + 'label_loc' => 'Français', + 'icon' => '/images/language-flags/fr.png', + ], + 'it' => [ + 'label_en' => 'Italian', + 'label_loc' => 'Italiano', + 'icon' => '/images/language-flags/it.png', + ], + 'ja' => [ + 'label_en' => 'Japanese', + 'label_loc' => '日本語', + 'icon' => '/images/language-flags/ja.png', + ], + 'pt_BR' => [ + 'label_en' => 'Brazilian Portuguese', + 'label_loc' => 'Português (Brasil)', + 'icon' => '/images/language-flags/br.png', + ], + 'ru' => [ + 'label_en' => 'Russian', + 'label_loc' => 'Русский', + 'icon' => '/images/language-flags/ru.png', + ], + 'tr' => [ + 'label_en' => 'Turkish', + 'label_loc' => 'Türkçe', + 'icon' => '/images/language-flags/tr.png', + ], + 'uk' => [ + 'label_en' => 'Ukrainian', + 'label_loc' => 'Українська', + 'icon' => '/images/language-flags/uk.webp', + ], + 'zh' => [ + 'label_en' => 'Chinese (Simplified)', + 'label_loc' => '简体中文', + 'icon' => '/images/language-flags/zh.webp', + ], + ]; + /** * Convert between language codes back and forth * diff --git a/src/Navigation/NavCardItem.php b/src/Navigation/NavCardItem.php new file mode 100644 index 0000000000..22beef36dd --- /dev/null +++ b/src/Navigation/NavCardItem.php @@ -0,0 +1,16 @@ +title = $title; return $this; @@ -96,6 +98,14 @@ public function setContent(string $content): self { return $this; } + public function setSummary(string $content): self { + if (empty($content)) { + throw new \Exception('Summary must not be empty'); + } + $this->summary = $content; + return $this; + } + public function getId(): string { return $this->id; } @@ -138,6 +148,10 @@ public function save(): self { $content = self::ce($dom, "content", null, [], $item); + if ($this->summary !== '') { + self::ce($dom, "summary", $this->summary, [], $item); + } + // Slurp content into our DOM. $tdoc = new \DOMDocument("1.0", "utf-8"); $tdoc->formatOutput = true; diff --git a/styles/landing.css b/styles/landing.css new file mode 100644 index 0000000000..0518abf863 --- /dev/null +++ b/styles/landing.css @@ -0,0 +1,416 @@ +.landing-hdr { + +} +/* + * HEADER + * Contains the giant PHP and our 3x lead elements + */ + +@media (min-width: 901px) { + .landing-hdr { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 1.5em; + } +} + +@media (max-width: 900px) { + .landing-hdr { + display: flex; + flex-direction: column; + gap: 1em; + } +} + +.landing-hdr-block { + padding: 1em; +} + +.landing-hdr-block + .landing-hdr-block { + border-top: 1px dashed #4a5568; +} + +.landing-hdr-title { + font-size: larger; + margin-bottom: 0.25em; +} + +.landing-hdr-tagline { + margin-bottom: 0; + font-size: 24px; +} + +.landing-hdr-content { + +} + +/* + * VCARDS + * The primary card layout used on the page; automatically collapses into a denser list on mobile. + */ + +.vgrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr)); + gap: 1.5em; + margin: 0; + padding: 0; + list-style-type: none; +} + +.vgrid-card { + border-radius: 1em; + overflow: hidden; +} + +@media (max-width: 700px) { + .vgrid { + gap: 0; + overflow: hidden; + border-radius: 1em; + animation: ease; + } + + .vgrid-card { + border-radius: 0 !important; + overflow: hidden; + } + + .vgrid-card + .vgrid-card { + margin-top: 3px; + } +} + +.landing-ver-hero-card { + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +.landing-ver-hero-card-inner { + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: hidden; + background: #555555; + color: white; + gap: 1em; + padding: 1em; +} + +.landing-ver-hero-img { + width: 100%; + height: 60px; + object-fit: contain; + object-position: center center; + margin-top: 1em; + margin-bottom: 1em; +} + +@media (max-width: 400px) { + .landing-ver-hero-img { + height: 40px; + margin-top: 0.25em; + margin-bottom: 0.25em; + } +} + +.landing-ver-hero-featuring { + font-weight: bold; + margin-bottom: 0.25em; +} + +.landing-ver-hero-latest { + display: flex; + flex-direction: column; + gap: 0.5em; + text-align: center; +} + +.landing-ver-hero-features { + margin-bottom: 0; +} + +.landing-ver-hero-label { + display: inline-flex; + padding: 0.25em 0.75em; + border-radius: 0.5em; + font-size: 90%; +} + +.landing-ver-hero-buttons { + display: flex; + flex-direction: column; + gap: 0.25em; + width: 100%; +} + +/* + * CARD LAYOUT + */ + +.landing-cc-card { + all: unset; + background: #3c4053; + border-radius: 1em; + display: flex; + flex-direction: row; + flex-grow: 1; + gap: 0.25em; + overflow: hidden; + cursor: pointer; + color: white; + outline: 3px transparent; + transition: all 0.2s ease-in-out; +} + +.landing-cc-card:hover { + outline: 3px solid #53576d; + background: #53576d; + overflow: hidden;; + transition: all 0.2s ease-in-out; +} + +.landing-cc-card:focus { + outline: 3px solid #eeeeee; +} + +.landing-cc-card-grey { + background: #444444 !important; +} + +.landing-cc-card-grey:hover { + background: #555555 !important; + outline: 3px solid #555555; +} + +.landing-cc-card-img { + height: 80px; + width: 80px; + object-fit: contain; + overflow: hidden; +} + +.landing-cc-card-content { + color: #eeeeee; + padding: 1em; + flex: 1 1; + display: flex; + flex-direction: column; + gap: 1em; +} + +.landing-cc-card-title { + color: white; + font-size: 125%; + font-weight: 500; +} + +.landing-cc-card-body { + flex: 1 1; +} + +/* + * LAST RELEASE HERO CARD + */ + +.landing-lrv { + overflow: hidden; + position: relative; + background: #333333; + border-radius: 1em; + margin: 0 auto 2em; + width: min(1440px, 100%); + border: 1px solid #555555; +} + +.landing-lrv-inner-padding { + position: relative; +} + +@media (max-width: 900px) { + .landing-lrv-inner-padding { + padding: 1em; + } + + .landing-lrv-inner { + display: flex; + flex-direction: column; + gap: 2em; + } +} + +@media (min-width: 901px) { + .landing-lrv-inner-padding { + padding: 2em; + } + + .landing-lrv-inner { + display: grid; + gap: 2em; + align-items: center; + grid-template-columns: 1fr 1fr; + } +} + +.landing-lrv-highlights { + display: grid; + gap: 1em; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); +} + +.landing-lrv-highlight { + background: #44444477; + color: white; + border-radius: 0.5em; + padding: 1em; + font-size: smaller; +} + +.landing-lrv-highlight-title { + font-weight: 500; +} + +/* + * ECOSYSTEM BANNER + * Full-width banner intended to promote ecosystem components as a single element + */ + +.landing-eco-banner { + display: flex; + flex-direction: row; + gap: 2em; + align-items: center; + padding: 0 5em; +} + +@media (max-width: 700px) { + .landing-eco-banner { + flex-direction: column; + padding: 0; + gap: 1em; + text-align: center; + } +} + +.landing-eco-text { + font-size: 24px; + line-height: 1.3; +} + +/* + * SECTIONS + */ + +.landing-section { + width: min(1440px, 100%); + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 2em; + padding: 5em 1em; + box-sizing: border-box; +} + +@media (max-width: 600px) { + .landing-section { + padding: 1em 1em; + } +} + +.landing-section-header { + font-size: 18px; + text-align: center; + text-decoration: none !important; + margin: 0; + padding: 0; + color: white; + line-height: 1.3; + font-weight: 500; +} + + +/* + * MICRO LABEL + * Key-Value label intended to be used for versions + */ + +.landing-ml { + font-size: smaller; + border-radius: 0.75em; + border: 1px solid #77777755; + overflow: hidden; + display: inline-flex; + align-items: center; +} + +.landing-ml-label { + padding: 0.15em 0.5em; + background: #00000044; + border-right: 1px solid #777777; +} + +.landing-ml-value { + padding: 0.15em 0.5em; +} + +/* + * CARD BUTTON + */ + +.landing-card-btn { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + box-sizing: border-box; + + /* "Chunk" styling: chunky padding and thick borders */ + padding: 14px 32px; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 1.1rem; + font-weight: 700; + text-decoration: none; + text-align: center; + letter-spacing: 0.5px; + cursor: pointer; + + /* Colors & Border */ + color: #111111 !important; + background-color: #ffffff; + border: 2px solid #111111; + border-radius: 0.5em; + + /* Smooth transitions for hover/active states */ + transition: all 0.2s ease-in-out; +} + +@media (max-width: 600px) { + .landing-card-btn { + padding: 4px 16px !important; + } +} + +/* Hover state */ +.landing-card-ovh:hover .landing-card-btn, +.landing-card-ovh:active .landing-card-btn, +.landing-card-btn:hover { + color: #ffffff !important; + background-color: #111111; + /* Shifts the button slightly and expands shadow for a "lifting" effect */ + transform: translate(-2px, -2px); + box-shadow: 6px 6px 0px 0px #000000; +} + +/* Focus state for accessibility */ +.landing-card-btn:focus-visible { + outline: 4px solid #818cf8; +} + +.hero-cards-bg { + transition: all 0.5s ease-in-out; + position: absolute; + inset: 0; + opacity: 0; +} +