1mod functions;
2mod helpers;
3mod types;
4
5use crate::type_map::PhpMapper;
6use ahash::AHashSet;
7use alef_codegen::builder::RustFileBuilder;
8use alef_codegen::conversions::ConversionConfig;
9use alef_codegen::generators::RustBindingConfig;
10use alef_codegen::generators::{self, AsyncPattern};
11use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile};
12use alef_core::config::{AlefConfig, Language, detect_serde_available, resolve_output_dir};
13use alef_core::ir::ApiSurface;
14use alef_core::ir::{PrimitiveType, TypeRef};
15use heck::{ToLowerCamelCase, ToPascalCase};
16use std::path::PathBuf;
17
18use functions::{gen_async_function, gen_function};
19use helpers::{
20 gen_enum_tainted_from_binding_to_core, gen_serde_bridge_from, gen_tokio_runtime, has_enum_named_field,
21 references_named_type,
22};
23use types::{gen_enum_constants, gen_opaque_struct_methods, gen_php_struct, gen_struct_methods};
24
25pub struct PhpBackend;
26
27impl PhpBackend {
28 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
29 RustBindingConfig {
30 struct_attrs: &["php_class"],
31 field_attrs: &[],
32 struct_derives: &["Clone"],
33 method_block_attr: Some("php_impl"),
34 constructor_attr: "",
35 static_attr: None,
36 function_attr: "#[php_function]",
37 enum_attrs: &[],
38 enum_derives: &[],
39 needs_signature: false,
40 signature_prefix: "",
41 signature_suffix: "",
42 core_import,
43 async_pattern: AsyncPattern::TokioBlockOn,
44 has_serde,
45 type_name_prefix: "",
46 option_duration_on_defaults: true,
47 }
48 }
49}
50
51impl Backend for PhpBackend {
52 fn name(&self) -> &str {
53 "php"
54 }
55
56 fn language(&self) -> Language {
57 Language::Php
58 }
59
60 fn capabilities(&self) -> Capabilities {
61 Capabilities {
62 supports_async: true,
63 supports_classes: true,
64 supports_enums: true,
65 supports_option: true,
66 supports_result: true,
67 ..Capabilities::default()
68 }
69 }
70
71 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
72 let enum_names = api.enums.iter().map(|e| e.name.clone()).collect();
73 let mapper = PhpMapper { enum_names };
74 let core_import = config.core_import();
75
76 let output_dir = resolve_output_dir(
77 config.output.php.as_ref(),
78 &config.crate_config.name,
79 "crates/{name}-php/src/",
80 );
81 let has_serde = detect_serde_available(&output_dir);
82 let cfg = Self::binding_config(&core_import, has_serde);
83
84 let mut builder = RustFileBuilder::new();
86 builder.add_import("ext_php_rs::prelude::*");
87
88 if has_serde {
90 builder.add_import("serde_json");
91 }
92
93 for trait_path in generators::collect_trait_imports(api) {
95 builder.add_import(&trait_path);
96 }
97
98 let has_maps = api.types.iter().any(|t| {
100 t.fields
101 .iter()
102 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
103 }) || api
104 .functions
105 .iter()
106 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
107 if has_maps {
108 builder.add_import("std::collections::HashMap");
109 }
110
111 let custom_mods = config.custom_modules.for_language(Language::Php);
113 for module in custom_mods {
114 builder.add_item(&format!("pub mod {module};"));
115 }
116
117 let has_async =
119 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
120
121 if has_async {
122 builder.add_item(&gen_tokio_runtime());
123 }
124
125 let opaque_types: AHashSet<String> = api
127 .types
128 .iter()
129 .filter(|t| t.is_opaque)
130 .map(|t| t.name.clone())
131 .collect();
132 if !opaque_types.is_empty() {
133 builder.add_import("std::sync::Arc");
134 }
135
136 let extension_name = config.php_extension_name();
139 let php_namespace = if extension_name.contains('_') {
140 let parts: Vec<&str> = extension_name.split('_').collect();
141 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
142 ns_parts.join("\\")
143 } else {
144 extension_name.to_pascal_case()
145 };
146
147 for typ in &api.types {
148 if typ.is_opaque {
149 let php_name_attr = format!("php(name = \"{}\\\\{}\")", php_namespace, typ.name);
152 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
153 let opaque_cfg = RustBindingConfig {
154 struct_attrs: &opaque_attr_arr,
155 ..cfg
156 };
157 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
158 builder.add_item(&gen_opaque_struct_methods(typ, &mapper, &opaque_types, &core_import));
159 } else {
160 builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace)));
163 builder.add_item(&gen_struct_methods(
164 typ,
165 &mapper,
166 has_serde,
167 &core_import,
168 &opaque_types,
169 ));
170 }
171 }
172
173 for enum_def in &api.enums {
174 builder.add_item(&gen_enum_constants(enum_def));
175 }
176
177 for func in &api.functions {
178 if func.is_async {
179 builder.add_item(&gen_async_function(func, &mapper, &opaque_types, &core_import));
180 } else {
181 builder.add_item(&gen_function(func, &mapper, &opaque_types, &core_import));
182 }
183 }
184
185 let convertible = alef_codegen::conversions::convertible_types(api);
186 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
187 let enum_names_ref = &mapper.enum_names;
192 let php_conv_config = ConversionConfig {
193 cast_large_ints_to_i64: true,
194 enum_string_names: Some(enum_names_ref),
195 json_to_string: true,
196 include_cfg_metadata: false,
197 option_duration_on_defaults: true,
198 ..Default::default()
199 };
200 let mut enum_tainted: AHashSet<String> = AHashSet::new();
202 for typ in &api.types {
203 if has_enum_named_field(typ, enum_names_ref) {
204 enum_tainted.insert(typ.name.clone());
205 }
206 }
207 let mut changed = true;
209 while changed {
210 changed = false;
211 for typ in &api.types {
212 if !enum_tainted.contains(&typ.name)
213 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
214 {
215 enum_tainted.insert(typ.name.clone());
216 changed = true;
217 }
218 }
219 }
220 for typ in &api.types {
221 if !enum_tainted.contains(&typ.name)
223 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
224 {
225 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
226 typ,
227 &core_import,
228 &php_conv_config,
229 ));
230 } else if enum_tainted.contains(&typ.name) && has_serde {
231 builder.add_item(&gen_serde_bridge_from(typ, &core_import));
234 } else if enum_tainted.contains(&typ.name) {
235 builder.add_item(&gen_enum_tainted_from_binding_to_core(
239 typ,
240 &core_import,
241 enum_names_ref,
242 &enum_tainted,
243 &php_conv_config,
244 &api.enums,
245 ));
246 }
247 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
249 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
250 typ,
251 &core_import,
252 &opaque_types,
253 &php_conv_config,
254 ));
255 }
256 }
257
258 for error in &api.errors {
260 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
261 }
262
263 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
264
265 let php_config = config.php.as_ref();
267 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
268 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
269 builder.add_inner_attribute(&format!(
270 "cfg_attr(all(windows, target_env = \"msvc\", feature = \"{feature_name}\"), feature(abi_vectorcall))"
271 ));
272 }
273
274 builder.add_item("#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {\n module\n}");
276
277 let content = builder.build();
278
279 Ok(vec![GeneratedFile {
280 path: PathBuf::from(&output_dir).join("lib.rs"),
281 content,
282 generated_header: false,
283 }])
284 }
285
286 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
287 let extension_name = config.php_extension_name();
288 let class_name = extension_name.to_pascal_case();
289
290 let mut content = String::from("<?php\n");
292 content.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n");
293 content.push_str("declare(strict_types=1);\n\n");
294
295 let namespace = if extension_name.contains('_') {
297 let parts: Vec<&str> = extension_name.split('_').collect();
298 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
299 ns_parts.join("\\")
300 } else {
301 class_name.clone()
302 };
303
304 content.push_str(&format!("namespace {};\n\n", namespace));
305 content.push_str(&format!("final class {}\n", class_name));
306 content.push_str("{\n");
307
308 for func in &api.functions {
310 let method_name = func.name.to_lower_camel_case();
311 let return_php_type = php_type(&func.return_type);
312
313 content.push_str(" /**\n");
315 for line in func.doc.lines() {
316 if line.is_empty() {
317 content.push_str(" *\n");
318 } else {
319 content.push_str(&format!(" * {}\n", line));
320 }
321 }
322 if func.doc.is_empty() {
323 content.push_str(&format!(" * {}.\n", method_name));
324 }
325 content.push_str(" *\n");
326 for p in &func.params {
327 let ptype = php_phpdoc_type(&p.ty);
328 let nullable_prefix = if p.optional { "?" } else { "" };
329 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
330 }
331 let return_phpdoc = php_phpdoc_type(&func.return_type);
332 content.push_str(&format!(" * @return {}\n", return_phpdoc));
333 if func.error_type.is_some() {
334 content.push_str(&format!(" * @throws \\{}\\{}Exception\n", namespace, class_name));
335 }
336 content.push_str(" */\n");
337
338 content.push_str(&format!(" public static function {}(", method_name));
340
341 let params: Vec<String> = func
342 .params
343 .iter()
344 .map(|p| {
345 let ptype = php_type(&p.ty);
346 if p.optional {
347 format!("?{} ${} = null", ptype, p.name)
348 } else {
349 format!("{} ${}", ptype, p.name)
350 }
351 })
352 .collect();
353 content.push_str(¶ms.join(", "));
354 content.push_str(&format!("): {}\n", return_php_type));
355 content.push_str(" {\n");
356 let ext_func_name = if func.is_async {
359 format!("{}_async", func.name)
360 } else {
361 func.name.clone()
362 };
363 content.push_str(&format!(
364 " return \\{}({}); // delegate to extension function\n",
365 ext_func_name,
366 func.params
367 .iter()
368 .map(|p| format!("${}", p.name))
369 .collect::<Vec<_>>()
370 .join(", ")
371 ));
372 content.push_str(" }\n\n");
373 }
374
375 content.push_str("}\n");
376
377 let output_dir = config
381 .php
382 .as_ref()
383 .and_then(|p| p.stubs.as_ref())
384 .map(|s| s.output.to_string_lossy().to_string())
385 .unwrap_or_else(|| "packages/php/src/".to_string());
386
387 Ok(vec![GeneratedFile {
388 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
389 content,
390 generated_header: false,
391 }])
392 }
393
394 fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
395 let extension_name = config.php_extension_name();
396 let class_name = extension_name.to_pascal_case();
397
398 let namespace = if extension_name.contains('_') {
400 let parts: Vec<&str> = extension_name.split('_').collect();
401 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
402 ns_parts.join("\\")
403 } else {
404 class_name.clone()
405 };
406
407 let mut content = String::from("<?php\n");
408 content.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n");
409 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
410 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
411 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
412 content.push_str("declare(strict_types=1);\n\n");
413 content.push_str(&format!("namespace {} {{\n\n", namespace));
415
416 content.push_str(&format!(
418 "class {}Exception extends \\RuntimeException\n{{\n",
419 class_name
420 ));
421 content.push_str(" public function getErrorCode(): int { }\n");
422 content.push_str("}\n\n");
423
424 for typ in &api.types {
426 if typ.is_opaque {
427 if !typ.doc.is_empty() {
428 content.push_str("/**\n");
429 for line in typ.doc.lines() {
430 if line.is_empty() {
431 content.push_str(" *\n");
432 } else {
433 content.push_str(&format!(" * {}\n", line));
434 }
435 }
436 content.push_str(" */\n");
437 }
438 content.push_str(&format!("class {}\n{{\n", typ.name));
439 content.push_str("}\n\n");
441 }
442 }
443
444 for typ in &api.types {
446 if typ.is_opaque || typ.fields.is_empty() {
447 continue;
448 }
449 if !typ.doc.is_empty() {
450 content.push_str("/**\n");
451 for line in typ.doc.lines() {
452 if line.is_empty() {
453 content.push_str(" *\n");
454 } else {
455 content.push_str(&format!(" * {}\n", line));
456 }
457 }
458 content.push_str(" */\n");
459 }
460 content.push_str(&format!("class {}\n{{\n", typ.name));
461
462 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
466 sorted_fields.sort_by_key(|f| f.optional);
467
468 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
471 .iter()
472 .copied()
473 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
474 .collect();
475 if !array_fields.is_empty() {
476 content.push_str(" /**\n");
477 for f in &array_fields {
478 let phpdoc = php_phpdoc_type(&f.ty);
479 let nullable_prefix = if f.optional { "?" } else { "" };
480 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, phpdoc, f.name));
481 }
482 content.push_str(" */\n");
483 }
484
485 let params: Vec<String> = sorted_fields
486 .iter()
487 .map(|f| {
488 let ptype = php_type(&f.ty);
489 let nullable = if f.optional { format!("?{}", ptype) } else { ptype };
490 let default = if f.optional { " = null" } else { "" };
491 format!(" {} ${}{}", nullable, f.name, default)
492 })
493 .collect();
494 content.push_str(" public function __construct(\n");
495 content.push_str(¶ms.join(",\n"));
496 content.push_str("\n ) { }\n\n");
497
498 for field in &typ.fields {
500 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
501 let return_type = if field.optional {
502 format!("?{}", php_type(&field.ty))
503 } else {
504 php_type(&field.ty)
505 };
506 let getter_name = field.name.to_lower_camel_case();
507 if is_array {
509 let phpdoc = php_phpdoc_type(&field.ty);
510 let nullable_prefix = if field.optional { "?" } else { "" };
511 content.push_str(&format!(" /** @return {}{} */\n", nullable_prefix, phpdoc));
512 }
513 content.push_str(&format!(
514 " public function get{}(): {} {{ }}\n",
515 getter_name.to_pascal_case(),
516 return_type
517 ));
518 }
519
520 content.push_str("}\n\n");
521 }
522
523 for enum_def in &api.enums {
525 content.push_str(&format!("enum {}: string\n{{\n", enum_def.name));
526 for variant in &enum_def.variants {
527 content.push_str(&format!(" case {} = '{}';\n", variant.name, variant.name));
528 }
529 content.push_str("}\n\n");
530 }
531
532 content.push_str("} // end namespace\n\n");
534
535 content.push_str("namespace {\n\n");
538
539 for func in &api.functions {
540 let return_type = php_type_fq(&func.return_type, &namespace);
541 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
542 content.push_str("/**\n");
543 for p in &func.params {
545 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
546 let nullable_prefix = if p.optional { "?" } else { "" };
547 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
548 }
549 content.push_str(&format!(" * @return {}\n */\n", return_phpdoc));
550
551 let params: Vec<String> = func
552 .params
553 .iter()
554 .map(|p| {
555 let ptype = php_type_fq(&p.ty, &namespace);
556 if p.optional {
557 format!("?{} ${} = null", ptype, p.name)
558 } else {
559 format!("{} ${}", ptype, p.name)
560 }
561 })
562 .collect();
563 let stub_func_name = if func.is_async {
565 format!("{}_async", func.name)
566 } else {
567 func.name.clone()
568 };
569 content.push_str(&format!(
570 "function {}({}): {} {{ }}\n\n",
571 stub_func_name,
572 params.join(", "),
573 return_type
574 ));
575 }
576
577 content.push_str("}\n");
578
579 let output_dir = config
581 .php
582 .as_ref()
583 .and_then(|p| p.stubs.as_ref())
584 .map(|s| s.output.to_string_lossy().to_string())
585 .unwrap_or_else(|| "packages/php/stubs/".to_string());
586
587 Ok(vec![GeneratedFile {
588 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
589 content,
590 generated_header: false,
591 }])
592 }
593
594 fn build_config(&self) -> Option<BuildConfig> {
595 Some(BuildConfig {
596 tool: "cargo",
597 crate_suffix: "-php",
598 depends_on_ffi: false,
599 post_build: vec![],
600 })
601 }
602}
603
604fn php_phpdoc_type(ty: &TypeRef) -> String {
607 match ty {
608 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
609 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
610 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
611 _ => php_type(ty),
612 }
613}
614
615fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
617 match ty {
618 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
619 TypeRef::Map(k, v) => format!(
620 "array<{}, {}>",
621 php_phpdoc_type_fq(k, namespace),
622 php_phpdoc_type_fq(v, namespace)
623 ),
624 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
625 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
626 _ => php_type(ty),
627 }
628}
629
630fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
632 match ty {
633 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
634 TypeRef::Optional(inner) => {
635 let inner_type = php_type_fq(inner, namespace);
636 format!("?{}", inner_type)
637 }
638 _ => php_type(ty),
639 }
640}
641
642fn php_type(ty: &TypeRef) -> String {
644 match ty {
645 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
646 TypeRef::Primitive(p) => match p {
647 PrimitiveType::Bool => "bool".to_string(),
648 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
649 PrimitiveType::U8
650 | PrimitiveType::U16
651 | PrimitiveType::U32
652 | PrimitiveType::U64
653 | PrimitiveType::I8
654 | PrimitiveType::I16
655 | PrimitiveType::I32
656 | PrimitiveType::I64
657 | PrimitiveType::Usize
658 | PrimitiveType::Isize => "int".to_string(),
659 },
660 TypeRef::Optional(inner) => {
661 let inner_type = php_type(inner);
662 format!("?{}", inner_type)
663 }
664 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
665 TypeRef::Named(name) => name.clone(),
666 TypeRef::Unit => "void".to_string(),
667 TypeRef::Duration => "float".to_string(),
668 }
669}