1use std::fmt::Write;
2use std::path::Path;
3
4use scythe_backend::manifest::{BackendManifest, load_manifest};
5use scythe_backend::naming::{
6 enum_type_name, enum_variant_name, fn_name, row_struct_name, to_pascal_case,
7};
8
9use scythe_core::analyzer::{AnalyzedQuery, CompositeInfo, EnumInfo};
10use scythe_core::errors::{ErrorCode, ScytheError};
11use scythe_core::parser::QueryCommand;
12
13use crate::backend_trait::{CodegenBackend, ResolvedColumn, ResolvedParam};
14
15const DEFAULT_MANIFEST_PG: &str = include_str!("../../manifests/php-pdo.toml");
16const DEFAULT_MANIFEST_MYSQL: &str = include_str!("../../manifests/php-pdo.mysql.toml");
17const DEFAULT_MANIFEST_SQLITE: &str = include_str!("../../manifests/php-pdo.sqlite.toml");
18
19pub struct PhpPdoBackend {
20 manifest: BackendManifest,
21}
22
23impl PhpPdoBackend {
24 pub fn new(engine: &str) -> Result<Self, ScytheError> {
25 let default_toml = match engine {
26 "postgresql" | "postgres" | "pg" => DEFAULT_MANIFEST_PG,
27 "mysql" | "mariadb" => DEFAULT_MANIFEST_MYSQL,
28 "sqlite" | "sqlite3" => DEFAULT_MANIFEST_SQLITE,
29 _ => {
30 return Err(ScytheError::new(
31 ErrorCode::InternalError,
32 format!("unsupported engine '{}' for php-pdo backend", engine),
33 ));
34 }
35 };
36 let manifest_path = Path::new("backends/php-pdo/manifest.toml");
37 let manifest = if manifest_path.exists() {
38 load_manifest(manifest_path)
39 .map_err(|e| ScytheError::new(ErrorCode::InternalError, format!("manifest: {e}")))?
40 } else {
41 toml::from_str(default_toml)
42 .map_err(|e| ScytheError::new(ErrorCode::InternalError, format!("manifest: {e}")))?
43 };
44 Ok(Self { manifest })
45 }
46}
47
48fn rewrite_params(sql: &str) -> String {
50 let mut result = sql.to_string();
51 for i in (1..=99).rev() {
53 let from = format!("${}", i);
54 let to = format!(":p{}", i);
55 result = result.replace(&from, &to);
56 }
57 result
58}
59
60fn php_cast(neutral_type: &str) -> &'static str {
62 match neutral_type {
63 "int16" | "int32" | "int64" => "(int) ",
64 "float32" | "float64" => "(float) ",
65 "bool" => "(bool) ",
66 "string" | "json" | "inet" | "interval" | "uuid" | "decimal" | "bytes" => "(string) ",
67 _ => "",
68 }
69}
70
71impl CodegenBackend for PhpPdoBackend {
72 fn name(&self) -> &str {
73 "php-pdo"
74 }
75
76 fn manifest(&self) -> &scythe_backend::manifest::BackendManifest {
77 &self.manifest
78 }
79
80 fn supported_engines(&self) -> &[&str] {
81 &["postgresql", "mysql", "sqlite"]
82 }
83
84 fn file_header(&self) -> String {
85 "<?php\n\ndeclare(strict_types=1);\n\n// Auto-generated by scythe. Do not edit.\n"
86 .to_string()
87 }
88
89 fn generate_row_struct(
90 &self,
91 query_name: &str,
92 columns: &[ResolvedColumn],
93 ) -> Result<String, ScytheError> {
94 let struct_name = row_struct_name(query_name, &self.manifest.naming);
95 let mut out = String::new();
96
97 let _ = writeln!(out, "readonly class {} {{", struct_name);
99 let _ = writeln!(out, " public function __construct(");
100 for c in columns.iter() {
101 let sep = ",";
102 let _ = writeln!(
103 out,
104 " public {} ${}{}",
105 c.full_type, c.field_name, sep
106 );
107 }
108 let _ = writeln!(out, " ) {{}}");
109 let _ = writeln!(out);
110
111 let _ = writeln!(
113 out,
114 " public static function fromRow(array $row): self {{"
115 );
116 let _ = writeln!(out, " return new self(");
117 for c in columns.iter() {
118 let sep = ",";
119 let is_enum = c.neutral_type.starts_with("enum::");
120 let is_datetime = matches!(
121 c.neutral_type.as_str(),
122 "date" | "time" | "time_tz" | "datetime" | "datetime_tz"
123 );
124 if is_enum {
125 let enum_type = &c.lang_type;
127 if c.nullable {
128 let _ = writeln!(
129 out,
130 " {}: $row['{}'] !== null ? {}::from($row['{}']) : null{}",
131 c.field_name, c.name, enum_type, c.name, sep
132 );
133 } else {
134 let _ = writeln!(
135 out,
136 " {}: {}::from($row['{}']){}",
137 c.field_name, enum_type, c.name, sep
138 );
139 }
140 } else if is_datetime {
141 if c.nullable {
143 let _ = writeln!(
144 out,
145 " {}: $row['{}'] !== null ? new \\DateTimeImmutable($row['{}']) : null{}",
146 c.field_name, c.name, c.name, sep
147 );
148 } else {
149 let _ = writeln!(
150 out,
151 " {}: new \\DateTimeImmutable($row['{}']){}",
152 c.field_name, c.name, sep
153 );
154 }
155 } else {
156 let cast = php_cast(&c.neutral_type);
157 if c.nullable {
158 let _ = writeln!(
159 out,
160 " {}: $row['{}'] !== null ? {}{} : null{}",
161 c.field_name,
162 c.name,
163 cast,
164 format_args!("$row['{}']", c.name),
165 sep
166 );
167 } else {
168 let _ = writeln!(
169 out,
170 " {}: {}$row['{}']{}",
171 c.field_name, cast, c.name, sep
172 );
173 }
174 }
175 }
176 let _ = writeln!(out, " );");
177 let _ = writeln!(out, " }}");
178 let _ = write!(out, "}}");
179 Ok(out)
180 }
181
182 fn generate_model_struct(
183 &self,
184 table_name: &str,
185 columns: &[ResolvedColumn],
186 ) -> Result<String, ScytheError> {
187 let name = to_pascal_case(table_name);
188 self.generate_row_struct(&name, columns)
189 }
190
191 fn generate_query_fn(
192 &self,
193 analyzed: &AnalyzedQuery,
194 struct_name: &str,
195 _columns: &[ResolvedColumn],
196 params: &[ResolvedParam],
197 ) -> Result<String, ScytheError> {
198 let func_name = fn_name(&analyzed.name, &self.manifest.naming);
199 let sql = rewrite_params(&super::clean_sql_oneline(&analyzed.sql));
200 let mut out = String::new();
201
202 let param_list = params
204 .iter()
205 .map(|p| format!("{} ${}", p.full_type, p.field_name))
206 .collect::<Vec<_>>()
207 .join(", ");
208 let sep = if param_list.is_empty() { "" } else { ", " };
209
210 let return_type = match &analyzed.command {
212 QueryCommand::One => format!("?{}", struct_name),
213 QueryCommand::Many | QueryCommand::Batch => "array".to_string(),
214 QueryCommand::Exec => "void".to_string(),
215 QueryCommand::ExecResult | QueryCommand::ExecRows => "int".to_string(),
216 };
217
218 let _ = writeln!(
219 out,
220 "function {}(PDO $pdo{}{}): {} {{",
221 func_name, sep, param_list, return_type
222 );
223
224 let _ = writeln!(out, " $stmt = $pdo->prepare(\"{}\");", sql);
226
227 if params.is_empty() {
231 let _ = writeln!(out, " $stmt->execute();");
232 } else {
233 let use_positional = sql.contains('?');
234 let bindings = params
235 .iter()
236 .enumerate()
237 .map(|(i, p)| {
238 let value = if p.neutral_type.starts_with("enum::") {
239 format!("${}->value", p.field_name)
240 } else {
241 format!("${}", p.field_name)
242 };
243 if use_positional {
244 value
245 } else {
246 format!("\"p{}\" => {}", i + 1, value)
247 }
248 })
249 .collect::<Vec<_>>()
250 .join(", ");
251 let _ = writeln!(out, " $stmt->execute([{}]);", bindings);
252 }
253
254 match &analyzed.command {
255 QueryCommand::One => {
256 let _ = writeln!(out, " $row = $stmt->fetch(PDO::FETCH_ASSOC);");
257 let _ = writeln!(
258 out,
259 " return $row ? {}::fromRow($row) : null;",
260 struct_name
261 );
262 }
263 QueryCommand::Many | QueryCommand::Batch => {
264 let _ = writeln!(out, " $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);");
265 let _ = writeln!(
266 out,
267 " return array_map([{}::class, 'fromRow'], $rows);",
268 struct_name
269 );
270 }
271 QueryCommand::Exec => {
272 }
274 QueryCommand::ExecResult | QueryCommand::ExecRows => {
275 let _ = writeln!(out, " return $stmt->rowCount();");
276 }
277 }
278
279 let _ = write!(out, "}}");
280 Ok(out)
281 }
282
283 fn generate_enum_def(&self, enum_info: &EnumInfo) -> Result<String, ScytheError> {
284 let type_name = enum_type_name(&enum_info.sql_name, &self.manifest.naming);
285 let mut out = String::new();
286 let _ = writeln!(out, "enum {}: string {{", type_name);
287 for value in &enum_info.values {
288 let variant = enum_variant_name(value, &self.manifest.naming);
289 let _ = writeln!(out, " case {} = \"{}\";", variant, value);
290 }
291 let _ = write!(out, "}}");
292 Ok(out)
293 }
294
295 fn generate_composite_def(&self, composite: &CompositeInfo) -> Result<String, ScytheError> {
296 let name = to_pascal_case(&composite.sql_name);
297 let mut out = String::new();
298 let _ = writeln!(out, "readonly class {} {{", name);
299 let _ = writeln!(out, " public function __construct(");
300 if composite.fields.is_empty() {
301 } else {
303 for field in &composite.fields {
304 let _ = writeln!(out, " public mixed ${},", field.name);
305 }
306 }
307 let _ = writeln!(out, " ) {{}}");
308 let _ = write!(out, "}}");
309 Ok(out)
310 }
311}