Skip to main content

scythe_codegen/backends/
php_pdo.rs

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
39/// Rewrite $1, $2, ... to :p1, :p2, ...
40fn rewrite_params(sql: &str) -> String {
41    let mut result = sql.to_string();
42    // Replace from highest number down to avoid $1 matching inside $10
43    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
51/// Map a neutral type to a PHP cast expression.
52fn 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        // Readonly class with constructor
81        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        // fromRow factory method
95        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        // Build PHP parameter list
148        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        // Return type depends on command
156        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        // Prepare statement
170        let _ = writeln!(out, "    $stmt = $pdo->prepare(\"{}\");", sql);
171
172        // Build execute params
173        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                // nothing else needed
204            }
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            // empty constructor
233        } 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}