use crate::error::{Error, Result};
use serde_json::Value;
use std::path::{Path, PathBuf};
pub const AUTOLOAD_RUNTIME_TEMPLATE: &str = r#"<?php
// autoload_runtime.php @generated by Symfony Runtime
if (true === (require_once __DIR__.'/autoload.php') || empty($_SERVER['SCRIPT_FILENAME'])) {
return;
}
$app = require $_SERVER['SCRIPT_FILENAME'];
if (!is_object($app)) {
throw new TypeError(sprintf('Invalid return value: callable object expected, "%s" returned from "%s".', get_debug_type($app), $_SERVER['SCRIPT_FILENAME']));
}
if (is_string($_SERVER['APP_RUNTIME_OPTIONS'] ??= $_ENV['APP_RUNTIME_OPTIONS'] ?? [])) {
$_SERVER['APP_RUNTIME_OPTIONS'] = json_decode($_SERVER['APP_RUNTIME_OPTIONS'], true, 512, JSON_THROW_ON_ERROR);
}
$_SERVER['APP_RUNTIME'] ??= $_ENV['APP_RUNTIME'] ?? %runtime_class%;
$runtime = new $_SERVER['APP_RUNTIME']($_SERVER['APP_RUNTIME_OPTIONS'] += %runtime_options%);
[$app, $args] = $runtime
->getResolver($app)
->resolve();
$app = $app(...$args);
exit(
$runtime
->getRunner($app)
->run()
);
"#;
pub fn write_stub(vendor_dir: &Path, project_dir: &Path, root_manifest: &Value) -> Result<()> {
let extra = root_manifest.get("extra").and_then(|e| e.get("runtime"));
if extra == Some(&Value::Bool(false)) {
return Ok(());
}
let mut options: serde_json::Map<String, Value> = match extra {
Some(Value::Object(m)) => m.clone(),
_ => serde_json::Map::new(),
};
let template = match options.get("autoload_template").and_then(Value::as_str) {
Some(t) => {
let path = if crate::pathutil::is_absolute_path(t) {
PathBuf::from(t)
} else {
project_dir.join(t)
};
std::fs::read_to_string(&path).map_err(|_| {
Error::Unsupported(format!(
"File \"{t}\" defined under \"extra.runtime.autoload_template\" in your composer.json file not found."
))
})?
}
None => {
let shipped = vendor_dir.join("symfony/runtime/Internal/autoload_runtime.template");
std::fs::read_to_string(&shipped)
.unwrap_or_else(|_| AUTOLOAD_RUNTIME_TEMPLATE.to_owned())
}
};
let vendor_real =
std::fs::canonicalize(vendor_dir).unwrap_or_else(|_| vendor_dir.to_path_buf());
let sub = options
.get("project_dir")
.and_then(Value::as_str)
.unwrap_or("");
let target =
std::fs::canonicalize(project_dir.join(sub)).unwrap_or_else(|_| project_dir.join(sub));
let mut relative = make_path_relative(&target, &vendor_real);
let mut nesting = 0;
while let Some(rest) = relative.strip_prefix("../") {
nesting += 1;
relative = rest.to_owned();
}
let project_code = if nesting == 0 {
format!("__DIR__.{}", php_var_export_str(&format!("/{relative}")))
} else if relative.is_empty() {
format!("dirname(__DIR__, {nesting})")
} else {
format!(
"dirname(__DIR__, {nesting}).{}",
php_var_export_str(&format!("/{relative}"))
)
};
let class = options
.get("class")
.and_then(Value::as_str)
.unwrap_or("Symfony\\Component\\Runtime\\SymfonyRuntime")
.to_owned();
for k in ["class", "autoload_template", "project_dir"] {
options.remove(k);
}
let exported = php_var_export(&Value::Object(options), 0);
let inner = &exported[7..exported.len() - 1];
let runtime_options = format!("[{inner} 'project_dir' => {project_code},\n]");
let code = template
.replace("%project_dir%", &project_code)
.replace("%runtime_class%", &php_var_export_str(&class))
.replace("%runtime_options%", &runtime_options);
let path = vendor_dir.join("autoload_runtime.php");
if std::fs::read_to_string(&path).ok().as_deref() == Some(code.as_str()) {
return Ok(());
}
let tmp = vendor_dir.join(".autoload_runtime.php.vivacity-tmp");
std::fs::write(&tmp, code).map_err(Error::io(&tmp))?;
std::fs::rename(&tmp, &path).map_err(Error::io(&path))?;
Ok(())
}
fn make_path_relative(end: &Path, start: &Path) -> String {
let e: Vec<String> = end
.components()
.skip(1)
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect();
let st: Vec<String> = start
.components()
.skip(1)
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect();
let common = e.iter().zip(st.iter()).take_while(|(a, b)| a == b).count();
let mut out = "../".repeat(st.len() - common);
let rest = e[common..].join("/");
if !rest.is_empty() {
out.push_str(&rest);
out.push('/');
}
if out.is_empty() {
"./".to_owned()
} else {
out
}
}
fn php_var_export_str(s: &str) -> String {
format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'"))
}
pub fn php_var_export(v: &Value, indent: usize) -> String {
let pad = " ".repeat(indent);
match v {
Value::Null => "NULL".to_owned(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
i.to_string()
} else {
let f = n.as_f64().unwrap_or(0.0);
let s = crate::phpjson::php_double(f).unwrap_or_default();
if s.contains('.') || s.contains('E') || s.contains('e') {
s
} else {
format!("{s}.0")
}
}
}
Value::String(s) => php_var_export_str(s),
Value::Array(items) => {
let mut out = String::from("array (\n");
for (i, item) in items.iter().enumerate() {
out.push_str(&format!(
"{pad} {i} => {}",
php_var_export_nested(item, indent + 1)
));
}
out.push_str(&format!("{pad})"));
out
}
Value::Object(map) => {
let mut out = String::from("array (\n");
for (k, item) in map {
let key = match k.parse::<i64>() {
Ok(i) if i.to_string() == *k => i.to_string(),
_ => php_var_export_str(k),
};
out.push_str(&format!(
"{pad} {key} => {}",
php_var_export_nested(item, indent + 1)
));
}
out.push_str(&format!("{pad})"));
out
}
}
}
fn php_var_export_nested(v: &Value, indent: usize) -> String {
match v {
Value::Array(_) | Value::Object(_) => {
format!("\n{}{},\n", " ".repeat(indent), php_var_export(v, indent))
}
_ => format!("{},\n", php_var_export(v, indent)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn var_export_like_php() {
assert_eq!(php_var_export(&json!({}), 0), "array (\n)");
assert_eq!(
php_var_export(&json!({"debug": true, "env": "prod", "n": 3, "list": ["a"], "sub": {"k": null}}), 0),
"array (\n 'debug' => true,\n 'env' => 'prod',\n 'n' => 3,\n 'list' => \n array (\n 0 => 'a',\n ),\n 'sub' => \n array (\n 'k' => NULL,\n ),\n)"
);
}
#[test]
fn default_stub_matches_the_observed_output() {
let tmp = tempfile::tempdir().expect("tmp");
let vendor = tmp.path().join("vendor");
std::fs::create_dir_all(&vendor).expect("mkdir");
write_stub(&vendor, tmp.path(), &json!({})).expect("stub");
let code = std::fs::read_to_string(vendor.join("autoload_runtime.php")).expect("read");
assert!(code.contains("$_SERVER['APP_RUNTIME'] ??= $_ENV['APP_RUNTIME'] ?? 'Symfony\\\\Component\\\\Runtime\\\\SymfonyRuntime';"));
assert!(code.contains(
"$_SERVER['APP_RUNTIME_OPTIONS'] += [\n 'project_dir' => dirname(__DIR__, 1),\n]);"
));
write_stub(&vendor, tmp.path(), &json!({"extra": {"runtime": {"class": "App\\Runtime", "dotenv_path": ".env.local", "project_dir": "."}}})).expect("stub");
let code = std::fs::read_to_string(vendor.join("autoload_runtime.php")).expect("read");
assert!(code.contains("?? 'App\\\\Runtime';"), "{code}");
assert!(code.contains("+= [\n 'dotenv_path' => '.env.local',\n 'project_dir' => dirname(__DIR__, 1),\n]);"), "{code}");
}
}