astro-php-ssr
Version:
Run .php routes inside Astro SSR via php-cgi (or php fallback).
133 lines (113 loc) • 4.11 kB
JavaScript
// src/middleware.mjs
import { spawn } from "node:child_process";
import { join, resolve } from "node:path";
import { stat } from "node:fs/promises";
const PHP_BIN = process.env.ASTRO_PHP_BINARY || "php-cgi";
const PHP_DIR = process.env.ASTRO_PHP_DIR || resolve(process.cwd(), "src/php");
async function executeWithPhpCgi(scriptPath, request) {
const url = new URL(request.url);
const method = request.method;
let postData = "";
if (method === "POST") {
const contentType = request.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
postData = JSON.stringify(await request.json());
} else {
postData = await request.text();
}
}
const env = {
...process.env,
REQUEST_METHOD: method,
QUERY_STRING: url.search.slice(1),
REQUEST_URI: url.pathname,
SCRIPT_FILENAME: scriptPath,
SCRIPT_NAME: url.pathname,
SERVER_PROTOCOL: "HTTP/1.1",
GATEWAY_INTERFACE: "CGI/1.1",
REDIRECT_STATUS: "200",
CONTENT_TYPE: request.headers.get("content-type") || "",
CONTENT_LENGTH: postData ? Buffer.byteLength(postData).toString() : "0",
HTTP_HOST: url.host,
HTTP_USER_AGENT: request.headers.get("user-agent") || "",
HTTP_ACCEPT: request.headers.get("accept") || "*/*",
HTTP_COOKIE: request.headers.get("cookie") || "",
};
return new Promise((resolvePromise, rejectPromise) => {
const php = spawn(PHP_BIN, [scriptPath], {
env,
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
php.stdout.on("data", (d) => (stdout += d.toString()));
php.stderr.on("data", (d) => (stderr += d.toString()));
if (postData) php.stdin.write(postData);
php.stdin.end();
php.on("close", (code) => {
if (code !== 0 && stderr) {
rejectPromise(new Error(`php-cgi error: ${stderr}`));
return;
}
const [rawHeaders, ...bodyParts] = stdout.split("\r\n\r\n");
const body = bodyParts.join("\r\n\r\n");
const headers = new Headers();
rawHeaders
.split("\n")
.map((l) => l.trim())
.filter(Boolean)
.forEach((line) => {
const idx = line.indexOf(":");
if (idx > 0) {
const key = line.slice(0, idx).trim();
const val = line.slice(idx + 1).trim();
headers.set(key, val);
}
});
resolvePromise({ body, headers });
});
php.on("error", (err) => rejectPromise(err));
});
}
async function executeWithPhpCli(scriptPath) {
return new Promise((resolvePromise, rejectPromise) => {
const php = spawn("php", [scriptPath], { stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
php.stdout.on("data", (d) => (stdout += d.toString()));
php.stderr.on("data", (d) => (stderr += d.toString()));
php.on("close", (code) => {
if (code !== 0 && stderr) {
rejectPromise(new Error(`php error: ${stderr}`));
return;
}
resolvePromise({
body: stdout,
headers: new Headers({ "Content-Type": "text/html; charset=utf-8" }),
});
});
php.on("error", (err) => rejectPromise(err));
});
}
export async function onRequest({ request, url }, next) {
if (!url.pathname.endsWith(".php")) return next();
try {
const relative = url.pathname.replace(/^\/+/, "").replace(/^php\//, "");
const scriptPath = join(PHP_DIR, relative);
await stat(scriptPath); // throws if missing
try {
const { body, headers } = await executeWithPhpCgi(scriptPath, request);
return new Response(body, { status: 200, headers });
} catch (e) {
// fallback to php CLI
const { body, headers } = await executeWithPhpCli(scriptPath);
return new Response(body, { status: 200, headers });
}
} catch (err) {
console.error("[astro-php-ssr] PHP execution error:", err);
return new Response(
JSON.stringify({ error: "PHP execution failed", message: String(err?.message || err) }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}