diff --git a/electron/services/PhpManager.ts b/electron/services/PhpManager.ts
index 3d78050..0700fb1 100644
--- a/electron/services/PhpManager.ts
+++ b/electron/services/PhpManager.ts
@@ -36,6 +36,8 @@ interface AvailablePeclExtension {
downloadUrl: string;
description?: string;
packageName?: string; // Packagist 包名,用于 PIE 安装
+ supportedPhpVersions?: string[]; // 支持的 PHP 版本列表
+ notAvailableReason?: string; // 不可用原因
}
interface AvailablePhpVersion {
@@ -634,7 +636,7 @@ export class PhpManager {
}
/**
- * 获取可安装的 PECL 扩展列表(从 pecl.php.net 搜索)
+ * 获取可安装的 PECL 扩展列表(从 pecl.php.net 和 windows.php.net 获取)
*/
async getAvailableExtensions(
version: string,
@@ -652,83 +654,55 @@ export class PhpManager {
return this.getDefaultExtensionList(version);
}
- const { majorMinor, isNts } = phpInfo;
- console.log(`PHP Info: ${majorMinor}, NTS: ${isNts}`);
-
- // 使用 Packagist API 获取扩展列表(PIE 推荐方式)
- // https://packagist.org/extensions
- const keyword = searchKeyword || "";
- const searchUrl = keyword
- ? `https://packagist.org/search.json?q=${encodeURIComponent(
- keyword
- )}&type=php-ext`
- : `https://packagist.org/search.json?type=php-ext`;
-
- console.log(`从 Packagist 搜索扩展: ${keyword || "(全部)"}`);
-
- let foundPackages: {
- name: string;
- description: string;
- packageName: string;
- }[] = [];
-
- try {
- const jsonStr = await this.fetchHtmlContent(searchUrl);
- const data = JSON.parse(jsonStr);
-
- if (data.results && Array.isArray(data.results)) {
- for (const pkg of data.results) {
- // Packagist 包名格式:vendor/package
- const packageName = pkg.name || "";
- // 扩展名通常是包名的最后一部分,或从 description 提取
- let extName = packageName.split("/").pop() || "";
- // 移除常见前缀
- extName = extName
- .replace(/^php[-_]?/, "")
- .replace(/[-_]?extension$/, "");
-
- foundPackages.push({
- name: extName,
- description: pkg.description || "",
- packageName: packageName,
- });
- }
- }
- console.log(`从 Packagist 找到 ${foundPackages.length} 个扩展包`);
- } catch (e: any) {
- console.log(`Packagist API 请求失败: ${e.message},尝试使用预定义列表`);
- }
-
- // 如果 Packagist 无结果,使用预定义的常用扩展列表
- if (foundPackages.length === 0) {
- foundPackages = this.getPopularExtensionsList(keyword);
- console.log(`使用预定义扩展列表: ${foundPackages.length} 个`);
- }
+ const { majorMinor, isNts, compiler } = phpInfo;
+ console.log(
+ `PHP Info: ${majorMinor}, NTS: ${isNts}, Compiler: ${compiler}`
+ );
// 获取已安装的扩展
const installedExts = await this.getExtensions(version);
const installedNames = installedExts.map((e) => e.name.toLowerCase());
- // 过滤已安装的扩展
- const availablePackages = foundPackages.filter(
- (pkg) => !installedNames.includes(pkg.name.toLowerCase())
- );
+ // 从 PECL 统计页面爬取热门扩展列表
+ console.log(`[PECL] Fetching popular extensions from stats page...`);
+ let popularExtensions = await this.fetchPeclPopularExtensions();
- // 限制数量
- const checkPackages = availablePackages.slice(0, searchKeyword ? 50 : 20);
-
- for (const pkg of checkPackages) {
- extensions.push({
- name: pkg.name,
- version: "latest",
- downloadUrl: "", // PIE 会自动处理
- description: pkg.description,
- packageName: pkg.packageName,
- } as AvailablePeclExtension & { packageName?: string });
+ // 如果爬取失败,使用本地缓存列表
+ if (popularExtensions.length === 0) {
+ console.log(`[PECL] Using cached extension list`);
+ popularExtensions = this.getPeclExtensionsList();
}
- console.log(`找到 ${extensions.length} 个可安装的扩展`);
- return extensions.sort((a, b) => a.name.localeCompare(b.name));
+ // 搜索过滤
+ if (searchKeyword) {
+ const keyword = searchKeyword.toLowerCase();
+ popularExtensions = popularExtensions.filter(
+ (ext) =>
+ ext.name.toLowerCase().includes(keyword) ||
+ ext.description.toLowerCase().includes(keyword)
+ );
+ }
+
+ console.log(`[PECL] Found ${popularExtensions.length} extensions`);
+
+ // 过滤已安装的扩展
+ const availableExts = popularExtensions.filter(
+ (ext) => !installedNames.includes(ext.name.toLowerCase())
+ );
+
+ // 直接返回列表,不检查 DLL(安装时再检查)
+ const limit = searchKeyword ? 50 : 30;
+ for (const ext of availableExts.slice(0, limit)) {
+ extensions.push({
+ name: ext.name,
+ version: "latest", // 安装时获取最新版本
+ downloadUrl: "", // 安装时获取下载链接
+ description: ext.description,
+ });
+ }
+
+ console.log(`[PECL] Showing ${extensions.length} extensions`);
+ return extensions;
} catch (error: any) {
console.error("获取可用扩展列表失败:", error);
return this.getDefaultExtensionList(version);
@@ -736,123 +710,298 @@ export class PhpManager {
}
/**
- * 获取常用 PHP 扩展列表(带 Packagist 包名)
- * PIE 包名格式参考: https://packagist.org/extensions
+ * 从 PECL 统计页面爬取热门扩展列表
+ * https://pecl.php.net/package-stats.php
*/
- private getPopularExtensionsList(
- keyword?: string
- ): { name: string; description: string; packageName: string }[] {
- // PIE 兼容的扩展包名(vendor/package 格式)
- const popularExtensions = [
- {
- name: "redis",
- description: "PHP extension for Redis",
- packageName: "phpredis/phpredis", // 正确的 PIE 包名
- },
- {
- name: "mongodb",
- description: "MongoDB driver for PHP",
- packageName: "mongodb/mongodb", // MongoDB 官方扩展
- },
- {
- name: "memcached",
- description: "PHP extension for Memcached",
- packageName: "php-memcached-dev/php-memcached",
- },
- {
- name: "imagick",
- description: "ImageMagick for PHP",
- packageName: "imagick/imagick",
- },
- {
- name: "xdebug",
- description: "Debugging and profiling for PHP",
- packageName: "xdebug/xdebug",
- },
- {
- name: "swoole",
- description: "High-performance coroutine framework",
- packageName: "openswoole/swoole", // OpenSwoole(PIE 兼容)
- },
- {
- name: "yaml",
- description: "YAML parser and emitter",
- packageName: "php/pecl-file_formats-yaml",
- },
- {
- name: "apcu",
- description: "APCu - APC User Cache",
- packageName: "apcu/apcu", // 正确的 PIE 包名
- },
- { name: "grpc", description: "gRPC for PHP", packageName: "grpc/grpc" },
- {
- name: "protobuf",
- description: "Protocol Buffers",
- packageName: "google/protobuf",
- },
- {
- name: "igbinary",
- description: "Binary serialization",
- packageName: "igbinary/igbinary",
- },
- {
- name: "msgpack",
- description: "MessagePack serialization",
- packageName: "msgpack/msgpack-php",
- },
- {
- name: "sodium",
- description: "Modern cryptography library",
- packageName: "php/pecl-crypto-sodium",
- },
- {
- name: "zip",
- description: "ZIP file support",
- packageName: "php/pecl-file_formats-zip",
- },
- {
- name: "rar",
- description: "RAR archive support",
- packageName: "php/pecl-file_formats-rar",
- },
- {
- name: "amqp",
- description: "AMQP messaging library",
- packageName: "php-amqp/php-amqp",
- },
- {
- name: "oauth",
- description: "OAuth consumer extension",
- packageName: "php/pecl-web_services-oauth",
- },
- {
- name: "ssh2",
- description: "SSH2 bindings",
- packageName: "php/pecl-networking-ssh2",
- },
- {
- name: "event",
- description: "Event-based I/O",
- packageName: "php/pecl-event",
- },
- {
- name: "uv",
- description: "libuv bindings",
- packageName: "amphp/ext-uv",
- },
- ];
+ private async fetchPeclPopularExtensions(): Promise<
+ { name: string; description: string }[]
+ > {
+ try {
+ const url = "https://pecl.php.net/package-stats.php";
+ const html = await this.fetchHtmlContent(url);
- if (!keyword) {
- return popularExtensions;
+ if (html.length < 1000) {
+ console.log(`[PECL] Stats page too short: ${html.length}`);
+ return [];
+ }
+
+ const extensions: { name: string; description: string }[] = [];
+
+ // 匹配扩展名: redis 或 [redis](/package/redis)
+ // HTML 格式: 扩展名
+ const extPattern =
+ /]*>\1<\/a>/gi;
+ let match;
+
+ while ((match = extPattern.exec(html)) !== null) {
+ const name = match[1];
+ // 排除一些非扩展的链接
+ if (name && !extensions.find((e) => e.name === name)) {
+ extensions.push({
+ name: name,
+ description: this.getExtensionDescription(name),
+ });
+ }
+ }
+
+ // 备用模式: 匹配 href="/package/xxx"
+ if (extensions.length === 0) {
+ const altPattern = /href="\/package\/([a-zA-Z0-9_]+)"/gi;
+ while ((match = altPattern.exec(html)) !== null) {
+ const name = match[1];
+ if (
+ name &&
+ !extensions.find((e) => e.name === name) &&
+ !["stats", "search", "changelog"].includes(name.toLowerCase())
+ ) {
+ extensions.push({
+ name: name,
+ description: this.getExtensionDescription(name),
+ });
+ }
+ }
+ }
+
+ console.log(
+ `[PECL] Scraped ${extensions.length} extensions from stats page`
+ );
+ return extensions;
+ } catch (error: any) {
+ console.error(`[PECL] Failed to fetch stats page: ${error.message}`);
+ return [];
}
+ }
- const lowerKeyword = keyword.toLowerCase();
- return popularExtensions.filter(
- (ext) =>
- ext.name.toLowerCase().includes(lowerKeyword) ||
- ext.description.toLowerCase().includes(lowerKeyword) ||
- ext.packageName.toLowerCase().includes(lowerKeyword)
- );
+ /**
+ * 获取扩展描述(本地映射)
+ */
+ private getExtensionDescription(name: string): string {
+ const descriptions: Record...
- // 2. 检查每行是否包含 DLL 链接和版本信息
+ // Debug: Check if version links exist after decoding
+ const hasPackageLink = packageHtml.includes(`/package/${extName}/`);
+ const hasWindowsLink = packageHtml.includes("/windows");
+ console.log(
+ `[PECL] ${extName}: hasPackageLink=${hasPackageLink}, hasWindowsLink=${hasWindowsLink}`
+ );
- let latestStableVersion: string | null = null;
- let latestBetaVersion: string | null = null;
+ // Extract version numbers from page - multiple patterns for robustness
+ const allVersions: string[] = [];
+ let match;
- // 方法1:直接搜索带 DLL 链接的版本
- // 格式: /package/xxx/VERSION/windows">...DLL
- const dllVersionRegex = /\/package\/[^/]+\/([\d.]+(?:RC\d+)?)\/windows/gi;
+ // Pattern 1: Match /package/extname/x.y.z in href
+ // href="/package/amqp/2.2.0" or href='/package/amqp/2.2.0'
+ const escapedExtName = extName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ const versionPattern1 = new RegExp(
+ `href=["']/package/${escapedExtName}/([\\d]+\\.[\\d]+(?:\\.[\\d]+)?(?:RC\\d+|beta\\d*|alpha\\d*)?)["'>]`,
+ "gi"
+ );
+ while ((match = versionPattern1.exec(packageHtml)) !== null) {
+ const ver = match[1];
+ if (!allVersions.includes(ver)) {
+ allVersions.push(ver);
+ console.log(`[PECL] ${extName}: found version ${ver} (pattern1)`);
+ }
+ }
+
+ // Pattern 2: Match version numbers in table cells >x.y.z