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_TOML: &str = include_str!("../../manifests/php-pdo.toml");
16
17pub struct PhpPdoBackend {
18 manifest: BackendManifest,
19}
20
21impl PhpPdoBackend {
22 pub fn new() -> Result<Self, ScytheError> {
23 let manifest_path = Path::new("backends/php-pdo/manifest.toml");
24 let manifest = if manifest_path.exists() {
25 load_manifest(manifest_path)
26 .map_err(|e| ScytheError::new(ErrorCode::InternalError, format!("manifest: {e}")))?
27 } else {
28 toml::from_str(DEFAULT_MANIFEST_TOML)
29 .map_err(|e| ScytheError::new(ErrorCode::InternalError, format!("manifest: {e}")))?
30 };
31 Ok(Self { manifest })
32 }
33
34 pub fn manifest(&self) -> &BackendManifest {
35 &self.manifest
36 }
37}
38
39fn rewrite_params(sql: &str) -> String {
41 let mut result = sql.to_string();
42 for i in (1..=99).rev() {
44 let from = format!("${}", i);
45 let to = format!(":p{}", i);
46 result = result.replace(&from, &to);
47 }
48 result
49}
50
51fn php_cast(neutral_type: &str) -> &'static str {
53 match neutral_type {
54 "int16" | "int32" | "int64" => "(int) ",
55 "float32" | "float64" => "(float) ",
56 "bool" => "(bool) ",
57 "string" | "json" | "inet" | "interval" | "uuid" | "decimal" | "bytes" => "(string) ",
58 _ => "",
59 }
60}
61
62impl CodegenBackend for PhpPdoBackend {
63 fn name(&self) -> &str {
64 "php-pdo"
65 }
66
67 fn file_header(&self) -> String {
68 "<?php\n\ndeclare(strict_types=1);\n\n// Auto-generated by scythe. Do not edit.\n"
69 .to_string()
70 }
71
72 fn generate_row_struct(
73 &self,
74 query_name: &str,
75 columns: &[ResolvedColumn],
76 ) -> Result<String, ScytheError> {
77 let struct_name = row_struct_name(query_name, &self.manifest.naming);
78 let mut out = String::new();
79
80 let _ = writeln!(out, "readonly class {} {{", struct_name);
82 let _ = writeln!(out, " public function __construct(");
83 for c in columns.iter() {
84 let sep = ",";
85 let _ = writeln!(
86 out,
87 " public {} ${}{}",
88 c.full_type, c.field_name, sep
89 );
90 }
91 let _ = writeln!(out, " ) {{}}");
92 let _ = writeln!(out);
93
94 let _ = writeln!(
96 out,
97 " public static function fromRow(array $row): self {{"
98 );
99 let _ = writeln!(out, " return new self(");
100 for c in columns.iter() {
101 let sep = ",";
102 let cast = php_cast(&c.neutral_type);
103 if c.nullable {
104 let _ = writeln!(
105 out,
106 " {}: $row['{}'] !== null ? {}{} : null{}",
107 c.field_name,
108 c.name,
109 cast,
110 format_args!("$row['{}']", c.name),
111 sep
112 );
113 } else {
114 let _ = writeln!(
115 out,
116 " {}: {}$row['{}']{}",
117 c.field_name, cast, c.name, sep
118 );
119 }
120 }
121 let _ = writeln!(out, " );");
122 let _ = writeln!(out, " }}");
123 let _ = write!(out, "}}");
124 Ok(out)
125 }
126
127 fn generate_model_struct(
128 &self,
129 table_name: &str,
130 columns: &[ResolvedColumn],
131 ) -> Result<String, ScytheError> {
132 let name = to_pascal_case(table_name);
133 self.generate_row_struct(&name, columns)
134 }
135
136 fn generate_query_fn(
137 &self,
138 analyzed: &AnalyzedQuery,
139 struct_name: &str,
140 _columns: &[ResolvedColumn],
141 params: &[ResolvedParam],
142 ) -> Result<String, ScytheError> {
143 let func_name = fn_name(&analyzed.name, &self.manifest.naming);
144 let sql = rewrite_params(&super::clean_sql(&analyzed.sql));
145 let mut out = String::new();
146
147 let param_list = params
149 .iter()
150 .map(|p| format!("{} ${}", p.full_type, p.field_name))
151 .collect::<Vec<_>>()
152 .join(", ");
153 let sep = if param_list.is_empty() { "" } else { ", " };
154
155 let return_type = match &analyzed.command {
157 QueryCommand::One => format!("?{}", struct_name),
158 QueryCommand::Many | QueryCommand::Batch => "array".to_string(),
159 QueryCommand::Exec => "void".to_string(),
160 QueryCommand::ExecResult | QueryCommand::ExecRows => "int".to_string(),
161 };
162
163 let _ = writeln!(
164 out,
165 "function {}(PDO $pdo{}{}): {} {{",
166 func_name, sep, param_list, return_type
167 );
168
169 let _ = writeln!(out, " $stmt = $pdo->prepare(\"{}\");", sql);
171
172 if params.is_empty() {
174 let _ = writeln!(out, " $stmt->execute();");
175 } else {
176 let bindings = params
177 .iter()
178 .enumerate()
179 .map(|(i, p)| format!("\"p{}\" => ${}", i + 1, p.field_name))
180 .collect::<Vec<_>>()
181 .join(", ");
182 let _ = writeln!(out, " $stmt->execute([{}]);", bindings);
183 }
184
185 match &analyzed.command {
186 QueryCommand::One => {
187 let _ = writeln!(out, " $row = $stmt->fetch(PDO::FETCH_ASSOC);");
188 let _ = writeln!(
189 out,
190 " return $row ? {}::fromRow($row) : null;",
191 struct_name
192 );
193 }
194 QueryCommand::Many | QueryCommand::Batch => {
195 let _ = writeln!(out, " $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);");
196 let _ = writeln!(
197 out,
198 " return array_map([{}::class, 'fromRow'], $rows);",
199 struct_name
200 );
201 }
202 QueryCommand::Exec => {
203 }
205 QueryCommand::ExecResult | QueryCommand::ExecRows => {
206 let _ = writeln!(out, " return $stmt->rowCount();");
207 }
208 }
209
210 let _ = write!(out, "}}");
211 Ok(out)
212 }
213
214 fn generate_enum_def(&self, enum_info: &EnumInfo) -> Result<String, ScytheError> {
215 let type_name = enum_type_name(&enum_info.sql_name, &self.manifest.naming);
216 let mut out = String::new();
217 let _ = writeln!(out, "enum {}: string {{", type_name);
218 for value in &enum_info.values {
219 let variant = enum_variant_name(value, &self.manifest.naming);
220 let _ = writeln!(out, " case {} = \"{}\";", variant, value);
221 }
222 let _ = write!(out, "}}");
223 Ok(out)
224 }
225
226 fn generate_composite_def(&self, composite: &CompositeInfo) -> Result<String, ScytheError> {
227 let name = to_pascal_case(&composite.sql_name);
228 let mut out = String::new();
229 let _ = writeln!(out, "readonly class {} {{", name);
230 let _ = writeln!(out, " public function __construct(");
231 if composite.fields.is_empty() {
232 } else {
234 for field in &composite.fields {
235 let _ = writeln!(out, " public mixed ${},", field.name);
236 }
237 }
238 let _ = writeln!(out, " ) {{}}");
239 let _ = write!(out, "}}");
240 Ok(out)
241 }
242}