Skip to content

Commit bcfa813

Browse files
committed
- Add versions.php to web app which shows available library versions
- Eat your own dog food by using Relay to cache packages.json
1 parent 2d4b603 commit bcfa813

4 files changed

Lines changed: 260 additions & 0 deletions

File tree

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,10 @@
1313
},
1414
"require-dev": {
1515
"heroku/heroku-buildpack-php": "dev-main"
16+
},
17+
"scripts": {
18+
"compile": [
19+
"./web/compile.sh"
20+
]
1621
}
1722
}

web/compile.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
set -e
3+
4+
RELAY_INI=`find $(php-config --ini-dir) -name "*-relay.ini"`
5+
6+
sed -i 's/^;\? \?relay.maxmemory =.*/relay.maxmemory = 100M/' $RELAY_INI
7+
sed -i 's/^;\? \?relay.eviction_policy =.*/relay.eviction_policy = lru/' $RELAY_INI

web/header.phtml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
</a>
3636
</div>
3737
</div>
38+
<div class="mt-4 flex justify-center">
39+
<a href="/" class="text-sm text-indigo-600 hover:underline m-2">Home</a>
40+
<a href="/versions.php" class="text-sm text-indigo-600 hover:underline m-2">See all versions</a>
41+
</div>
3842
</div>
3943
</div>
4044
</header>

web/versions.php

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
<?php
2+
// versions.php
3+
// Generates HTML tables of extension versions from hardcoded packages.json URLs.
4+
5+
// Hardcoded packages.json URLs (edit as needed)
6+
$urls = [
7+
'https://heroku-php-extensions.s3.amazonaws.com/dist-heroku-24-amd64-stable/packages.json',
8+
'https://heroku-php-extensions.s3.amazonaws.com/dist-heroku-22-stable/packages.json',
9+
'https://heroku-php-extensions.s3.amazonaws.com/dist-heroku-20-stable/packages.json',
10+
'https://heroku-php-extensions.s3.amazonaws.com/dist-heroku-18-stable/packages.json'
11+
];
12+
13+
$relay = new Relay\Relay;
14+
$relay->connect(getenv('UPSTASH_REDIS_REST_URL'), 6379);
15+
$relay->auth(['default', getenv('UPSTASH_REDIS_REST_TOKEN')]);
16+
17+
$table = new Relay\Table('packages');
18+
19+
function fetch_json(string $url, $table): ?array
20+
{
21+
if($table->exists($url)) {
22+
$cached = $table->get($url);
23+
if ($cached) {
24+
return json_decode($cached, true);
25+
}
26+
}
27+
$opts = [
28+
'http' => [
29+
'method' => 'GET',
30+
'header' => "User-Agent: PHP\r\n",
31+
'timeout' => 10,
32+
],
33+
];
34+
$context = stream_context_create($opts);
35+
$json = @file_get_contents($url, false, $context);
36+
if ($json === false) {
37+
error_log("Failed to fetch: $url");
38+
return null;
39+
}
40+
$table->set($url, $json);
41+
$data = json_decode($json, true);
42+
if (json_last_error() !== JSON_ERROR_NONE) {
43+
error_log("JSON decode error for $url: " . json_last_error_msg());
44+
return null;
45+
}
46+
return $data;
47+
}
48+
49+
function parse_packages(array $packages_data): array
50+
{
51+
$stacks = [];
52+
if (!isset($packages_data['packages'])) {
53+
return $stacks;
54+
}
55+
56+
$packages = $packages_data['packages'];
57+
$package_list = [];
58+
59+
// If associative array (package-name => [versions])
60+
$is_assoc = array_keys($packages) !== range(0, count($packages) - 1);
61+
if ($is_assoc) {
62+
foreach ($packages as $package_name => $versions) {
63+
if (is_array($versions)) {
64+
foreach ($versions as $version_info) {
65+
$package_list[] = $version_info;
66+
}
67+
}
68+
}
69+
} else {
70+
// Flatten lists
71+
foreach ($packages as $item) {
72+
if (is_array($item) && array_keys($item) === range(0, count($item) - 1)) {
73+
foreach ($item as $sub) {
74+
$package_list[] = $sub;
75+
}
76+
} else {
77+
$package_list[] = $item;
78+
}
79+
}
80+
}
81+
82+
foreach ($package_list as $version_info) {
83+
if (!is_array($version_info)) {
84+
continue;
85+
}
86+
$package_name = $version_info['name'] ?? null;
87+
if (!$package_name || strpos($package_name, 'heroku-sys/ext-') !== 0) {
88+
continue;
89+
}
90+
$ext_name = substr($package_name, strlen('heroku-sys/ext-'));
91+
$ext_version = $version_info['version'] ?? null;
92+
if (!$ext_version) {
93+
continue;
94+
}
95+
$require = $version_info['require'] ?? [];
96+
$php_req = $require['heroku-sys/php'] ?? null;
97+
$stack_req = $require['heroku-sys/heroku'] ?? null;
98+
if (!$php_req || !$stack_req) {
99+
continue;
100+
}
101+
102+
// Parse stack (e.g., ^22.0.0 -> 22 -> heroku-22)
103+
if (preg_match('/\^?(\d+)\./', $stack_req, $m)) {
104+
$stack = 'heroku-' . $m[1];
105+
} else {
106+
continue;
107+
}
108+
109+
// Parse PHP version like 7.3.* -> 7.3
110+
if (preg_match('/(\d+\.\d+)\.\*/', $php_req, $m2)) {
111+
$php_version = $m2[1];
112+
} else {
113+
continue;
114+
}
115+
116+
$stacks[$stack][$php_version][$ext_name][] = $ext_version;
117+
}
118+
119+
// Deduplicate and sort versions
120+
foreach ($stacks as $stack_name => $phps) {
121+
foreach ($phps as $php => $exts) {
122+
foreach ($exts as $ext => $versions) {
123+
$unique = array_values(array_unique($versions));
124+
usort($unique, function ($a, $b) {
125+
return version_compare($a, $b);
126+
});
127+
$stacks[$stack_name][$php][$ext] = $unique;
128+
}
129+
}
130+
}
131+
132+
return $stacks;
133+
}
134+
135+
// Merge stacks from multiple manifests
136+
$all_stacks = [];
137+
foreach ($urls as $url) {
138+
$data = fetch_json($url, $table);
139+
if ($data === null) {
140+
continue;
141+
}
142+
$st = parse_packages($data);
143+
// Merge into all_stacks
144+
foreach ($st as $stack => $phps) {
145+
foreach ($phps as $php => $exts) {
146+
foreach ($exts as $ext => $versions) {
147+
if (!isset($all_stacks[$stack][$php][$ext])) {
148+
$all_stacks[$stack][$php][$ext] = [];
149+
}
150+
$all_stacks[$stack][$php][$ext] = array_merge($all_stacks[$stack][$php][$ext], $versions);
151+
}
152+
}
153+
}
154+
}
155+
156+
// Final dedupe and sort after merge
157+
foreach ($all_stacks as $stack_name => $phps) {
158+
foreach ($phps as $php => $exts) {
159+
foreach ($exts as $ext => $versions) {
160+
$unique = array_values(array_unique($versions));
161+
usort($unique, function ($a, $b) {
162+
return version_compare($a, $b);
163+
});
164+
$all_stacks[$stack_name][$php][$ext] = $unique;
165+
}
166+
}
167+
}
168+
169+
// Helper to sort php versions like 7.3, 8.0 numerically
170+
function sort_php_versions(array $versions): array
171+
{
172+
usort($versions, function ($a, $b) {
173+
$pa = array_map('intval', explode('.', $a));
174+
$pb = array_map('intval', explode('.', $b));
175+
for ($i = 0; $i < max(count($pa), count($pb)); $i++) {
176+
$va = $pa[$i] ?? 0;
177+
$vb = $pb[$i] ?? 0;
178+
if ($va === $vb) continue;
179+
return $va <=> $vb;
180+
}
181+
return 0;
182+
});
183+
return $versions;
184+
}
185+
186+
// Render HTML using site header/footer and Tailwind styling
187+
ini_set('display_errors', 1);
188+
error_reporting(E_ALL);
189+
190+
require __DIR__ . '/header.phtml';
191+
192+
if (empty($all_stacks)) {
193+
echo "<section class=\"max-w-2xl mx-auto\"><p class=\"text-sm text-gray-500\">No data available.</p></section>\n";
194+
require __DIR__ . '/footer.phtml';
195+
exit;
196+
}
197+
198+
foreach ($all_stacks as $stack => $phps) {
199+
// gather all extensions and php versions
200+
$all_extensions = [];
201+
$all_php_versions = [];
202+
foreach ($phps as $php => $exts) {
203+
$all_php_versions[] = $php;
204+
foreach ($exts as $ext => $_) {
205+
$all_extensions[$ext] = true;
206+
}
207+
}
208+
$all_extensions = array_keys($all_extensions);
209+
sort($all_extensions, SORT_NATURAL | SORT_FLAG_CASE);
210+
$all_php_versions = array_values(array_unique($all_php_versions));
211+
$all_php_versions = sort_php_versions($all_php_versions);
212+
213+
echo "<section class=\"max-w-4xl mx-auto mt-6\">\n";
214+
echo " <h3 class=\"text-lg leading-6 font-medium text-gray-900\">" . htmlspecialchars($stack) . "</h3>\n";
215+
echo " <div class=\"mt-4 overflow-x-auto\">\n";
216+
echo " <table class=\"min-w-full divide-y divide-gray-200\">\n";
217+
echo " <thead class=\"bg-gray-50\"><tr><th class=\"px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">PHP</th>";
218+
foreach ($all_extensions as $ext) {
219+
echo "<th class=\"px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">" . htmlspecialchars($ext) . "</th>";
220+
}
221+
echo "</tr></thead>\n";
222+
echo " <tbody class=\"bg-white divide-y divide-gray-200\">\n";
223+
224+
foreach ($all_php_versions as $php_ver) {
225+
echo " <tr>\n";
226+
echo " <td class=\"px-3 py-2 text-sm text-gray-700\">" . htmlspecialchars($php_ver) . "</td>";
227+
foreach ($all_extensions as $ext) {
228+
$versions = $all_stacks[$stack][$php_ver][$ext] ?? [];
229+
$cells = [];
230+
foreach ($versions as $v) {
231+
$cells[] = '<code class="text-xs bg-gray-100 px-2 py-1 rounded inline-block m-px">' . htmlspecialchars($v) . '</code>';
232+
}
233+
echo "<td class=\"px-3 py-2 text-sm text-gray-700\">" . implode(' ', $cells) . "</td>";
234+
}
235+
echo "</tr>\n";
236+
}
237+
238+
echo " </tbody>\n";
239+
echo " </table>\n";
240+
echo " </div>\n";
241+
echo "</section>\n";
242+
}
243+
244+
require __DIR__ . '/footer.phtml';

0 commit comments

Comments
 (0)