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, BuildDependency, Capabilities, GeneratedFile};
12use alef_core::config::{Language, ResolvedCrateConfig, detect_serde_available, resolve_output_dir};
13use alef_core::hash::{self, CommentStyle};
14use alef_core::ir::ApiSurface;
15use alef_core::ir::{PrimitiveType, TypeRef};
16use heck::{ToLowerCamelCase, ToPascalCase};
17use minijinja::context;
18use std::path::PathBuf;
19
20use crate::naming::php_autoload_namespace;
21use functions::{gen_async_function_as_static_method, gen_function_as_static_method};
22
23fn sanitize_php_enum_case(name: &str) -> String {
26 if name.eq_ignore_ascii_case("class") {
27 format!("{name}_")
28 } else {
29 name.to_string()
30 }
31}
32use helpers::{gen_enum_tainted_from_binding_to_core, gen_tokio_runtime, has_enum_named_field, references_named_type};
33use types::{
34 gen_enum_constants, gen_flat_data_enum, gen_flat_data_enum_from_impls, gen_flat_data_enum_methods,
35 gen_opaque_struct_methods, gen_php_struct, is_tagged_data_enum,
36};
37
38pub struct PhpBackend;
39
40impl PhpBackend {
41 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
42 RustBindingConfig {
43 struct_attrs: &["php_class"],
44 field_attrs: &[],
45 struct_derives: &["Clone"],
46 method_block_attr: Some("php_impl"),
47 constructor_attr: "",
48 static_attr: None,
49 function_attr: "#[php_function]",
50 enum_attrs: &[],
51 enum_derives: &[],
52 needs_signature: false,
53 signature_prefix: "",
54 signature_suffix: "",
55 core_import,
56 async_pattern: AsyncPattern::TokioBlockOn,
57 has_serde,
58 type_name_prefix: "",
59 option_duration_on_defaults: true,
60 opaque_type_names: &[],
61 skip_impl_constructor: false,
62 cast_uints_to_i32: false,
63 cast_large_ints_to_f64: false,
64 named_non_opaque_params_by_ref: false,
65 lossy_skip_types: &[],
66 }
67 }
68}
69
70impl Backend for PhpBackend {
71 fn name(&self) -> &str {
72 "php"
73 }
74
75 fn language(&self) -> Language {
76 Language::Php
77 }
78
79 fn capabilities(&self) -> Capabilities {
80 Capabilities {
81 supports_async: false,
82 supports_classes: true,
83 supports_enums: true,
84 supports_option: true,
85 supports_result: true,
86 ..Capabilities::default()
87 }
88 }
89
90 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
91 let data_enum_names: AHashSet<String> = api
93 .enums
94 .iter()
95 .filter(|e| is_tagged_data_enum(e))
96 .map(|e| e.name.clone())
97 .collect();
98 let enum_names: AHashSet<String> = api
99 .enums
100 .iter()
101 .filter(|e| !is_tagged_data_enum(e))
102 .map(|e| e.name.clone())
103 .collect();
104 let mapper = PhpMapper {
105 enum_names: enum_names.clone(),
106 data_enum_names: data_enum_names.clone(),
107 };
108 let core_import = config.core_import_name();
109
110 let php_config = config.php.as_ref();
112 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
113 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
114
115 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
116 let has_serde = detect_serde_available(&output_dir);
117
118 let bridge_type_aliases_php: Vec<String> = config
124 .trait_bridges
125 .iter()
126 .filter_map(|b| b.type_alias.clone())
127 .collect();
128 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
129 let mut opaque_names_vec_php: Vec<String> = api
130 .types
131 .iter()
132 .filter(|t| t.is_opaque)
133 .map(|t| t.name.clone())
134 .collect();
135 opaque_names_vec_php.extend(bridge_type_aliases_php);
136
137 let mut cfg = Self::binding_config(&core_import, has_serde);
138 cfg.opaque_type_names = &opaque_names_vec_php;
139
140 let mut builder = RustFileBuilder::new().with_generated_header();
142 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
143 builder.add_inner_attribute("allow(unsafe_code)");
144 builder.add_inner_attribute("allow(non_snake_case)");
146 builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions, clippy::arc_with_non_send_sync, clippy::collapsible_if, clippy::clone_on_copy)");
147 builder.add_import("ext_php_rs::prelude::*");
148
149 if has_serde {
151 builder.add_import("serde_json");
152 }
153
154 for trait_path in generators::collect_trait_imports(api) {
156 builder.add_import(&trait_path);
157 }
158
159 let has_maps = api.types.iter().any(|t| {
161 t.fields
162 .iter()
163 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
164 }) || api
165 .functions
166 .iter()
167 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
168 if has_maps {
169 builder.add_import("std::collections::HashMap");
170 }
171
172 let custom_mods = config.custom_modules.for_language(Language::Php);
174 for module in custom_mods {
175 builder.add_item(&format!("pub mod {module};"));
176 }
177
178 let has_async =
180 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
181
182 if has_async {
183 builder.add_item(&gen_tokio_runtime());
184 }
185
186 let opaque_types: AHashSet<String> = api
188 .types
189 .iter()
190 .filter(|t| t.is_opaque)
191 .map(|t| t.name.clone())
192 .collect();
193 if !opaque_types.is_empty() {
194 builder.add_import("std::sync::Arc");
195 }
196
197 let extension_name = config.php_extension_name();
200 let php_namespace = php_autoload_namespace(config);
201
202 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
204
205 for adapter in &config.adapters {
207 match adapter.pattern {
208 alef_core::config::AdapterPattern::Streaming => {
209 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
210 if let Some(struct_code) = adapter_bodies.get(&key) {
211 builder.add_item(struct_code);
212 }
213 }
214 alef_core::config::AdapterPattern::CallbackBridge => {
215 let struct_key = format!("{}.__bridge_struct__", adapter.name);
216 let impl_key = format!("{}.__bridge_impl__", adapter.name);
217 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
218 builder.add_item(struct_code);
219 }
220 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
221 builder.add_item(impl_code);
222 }
223 }
224 _ => {}
225 }
226 }
227
228 for typ in api
229 .types
230 .iter()
231 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
232 {
233 if typ.is_opaque {
234 let ns_escaped = php_namespace.replace('\\', "\\\\");
238 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
239 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
240 let opaque_cfg = RustBindingConfig {
241 struct_attrs: &opaque_attr_arr,
242 ..cfg
243 };
244 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
245 builder.add_item(&gen_opaque_struct_methods(
246 typ,
247 &mapper,
248 &opaque_types,
249 &core_import,
250 &adapter_bodies,
251 ));
252 } else {
253 builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace), &enum_names));
256 builder.add_item(&types::gen_struct_methods_with_exclude(
257 typ,
258 &mapper,
259 has_serde,
260 &core_import,
261 &opaque_types,
262 &enum_names,
263 &api.enums,
264 &exclude_functions,
265 &bridge_type_aliases_set,
266 ));
267 }
268 }
269
270 for enum_def in &api.enums {
271 if is_tagged_data_enum(enum_def) {
272 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
274 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
275 } else {
276 builder.add_item(&gen_enum_constants(enum_def));
277 }
278 }
279
280 let included_functions: Vec<_> = api
285 .functions
286 .iter()
287 .filter(|f| !exclude_functions.contains(&f.name))
288 .collect();
289 if !included_functions.is_empty() {
290 let facade_class_name = extension_name.to_pascal_case();
291 let mut method_items: Vec<String> = Vec::new();
294 for func in included_functions {
295 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
296 if let Some((param_idx, bridge_cfg)) = bridge_param {
297 method_items.push(crate::trait_bridge::gen_bridge_function(
298 func,
299 param_idx,
300 bridge_cfg,
301 &mapper,
302 &opaque_types,
303 &core_import,
304 ));
305 } else if func.is_async {
306 method_items.push(gen_async_function_as_static_method(
307 func,
308 &mapper,
309 &opaque_types,
310 &core_import,
311 &config.trait_bridges,
312 ));
313 } else {
314 method_items.push(gen_function_as_static_method(
315 func,
316 &mapper,
317 &opaque_types,
318 &core_import,
319 &config.trait_bridges,
320 has_serde,
321 ));
322 }
323 }
324
325 let methods_joined = method_items
326 .iter()
327 .map(|m| {
328 m.lines()
330 .map(|l| {
331 if l.is_empty() {
332 String::new()
333 } else {
334 format!(" {l}")
335 }
336 })
337 .collect::<Vec<_>>()
338 .join("\n")
339 })
340 .collect::<Vec<_>>()
341 .join("\n\n");
342 let php_api_class_name = format!("{facade_class_name}Api");
345 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
347 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
348 let facade_struct = format!(
349 "#[php_class]\n#[{php_name_attr}]\npub struct {facade_class_name}Api;\n\n#[php_impl]\nimpl {facade_class_name}Api {{\n{methods_joined}\n}}"
350 );
351 builder.add_item(&facade_struct);
352
353 for bridge_cfg in &config.trait_bridges {
355 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
356 let bridge = crate::trait_bridge::gen_trait_bridge(
357 trait_type,
358 bridge_cfg,
359 &core_import,
360 &config.error_type_name(),
361 &config.error_constructor_expr(),
362 api,
363 );
364 for imp in &bridge.imports {
365 builder.add_import(imp);
366 }
367 builder.add_item(&bridge.code);
368 }
369 }
370 }
371
372 let convertible = alef_codegen::conversions::convertible_types(api);
373 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
374 let input_types = alef_codegen::conversions::input_type_names(api);
375 let enum_names_ref = &mapper.enum_names;
380 let bridge_skip_types: Vec<String> = config
381 .trait_bridges
382 .iter()
383 .filter_map(|b| b.type_alias.clone())
384 .collect();
385 let php_conv_config = ConversionConfig {
386 cast_large_ints_to_i64: true,
387 enum_string_names: Some(enum_names_ref),
388 json_to_string: true,
389 include_cfg_metadata: false,
390 option_duration_on_defaults: true,
391 from_binding_skip_types: &bridge_skip_types,
392 ..Default::default()
393 };
394 let mut enum_tainted: AHashSet<String> = AHashSet::new();
396 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
397 if has_enum_named_field(typ, enum_names_ref) {
398 enum_tainted.insert(typ.name.clone());
399 }
400 }
401 let mut changed = true;
403 while changed {
404 changed = false;
405 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
406 if !enum_tainted.contains(&typ.name)
407 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
408 {
409 enum_tainted.insert(typ.name.clone());
410 changed = true;
411 }
412 }
413 }
414 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
415 if input_types.contains(&typ.name)
417 && !enum_tainted.contains(&typ.name)
418 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
419 {
420 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
421 typ,
422 &core_import,
423 &php_conv_config,
424 ));
425 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
426 builder.add_item(&gen_enum_tainted_from_binding_to_core(
433 typ,
434 &core_import,
435 enum_names_ref,
436 &enum_tainted,
437 &php_conv_config,
438 &api.enums,
439 &bridge_type_aliases_set,
440 ));
441 }
442 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
444 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
445 typ,
446 &core_import,
447 &opaque_types,
448 &php_conv_config,
449 ));
450 }
451 }
452
453 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
455 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
456 }
457
458 for error in &api.errors {
460 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
461 }
462
463 if has_serde {
467 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
468 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
469 pub fn max_compression_ratio() -> i64 { 100 }\n\
470 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
471 pub fn max_nesting_depth() -> i64 { 1024 }\n\
472 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
473 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
474 pub fn max_iterations() -> i64 { 10_000_000 }\n\
475 pub fn max_xml_depth() -> i64 { 1024 }\n\
476 pub fn max_table_cells() -> i64 { 100_000 }\n\
477 }";
478 builder.add_item(serde_module);
479 }
480
481 let php_config = config.php.as_ref();
487 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
488
489 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
493 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
494 }
495
496 let mut class_registrations = String::new();
499 for typ in api
500 .types
501 .iter()
502 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
503 {
504 class_registrations.push_str(&crate::template_env::render(
505 "php_class_registration.jinja",
506 context! { class_name => &typ.name },
507 ));
508 }
509 if !api.functions.is_empty() {
511 let facade_class_name = extension_name.to_pascal_case();
512 class_registrations.push_str(&crate::template_env::render(
513 "php_class_registration.jinja",
514 context! { class_name => &format!("{facade_class_name}Api") },
515 ));
516 }
517 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
520 class_registrations.push_str(&crate::template_env::render(
521 "php_class_registration.jinja",
522 context! { class_name => &enum_def.name },
523 ));
524 }
525 builder.add_item(&format!(
526 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
527 ));
528
529 let mut content = builder.build();
530
531 for bridge in &config.trait_bridges {
536 if let Some(field_name) = bridge.resolved_options_field() {
537 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
538 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
539 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
540 let builder_type = format!("{}Builder", options_type);
541 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
542
543 let old_method = format!(
549 " pub fn {field_name}(&self, {param_name}: Option<&{type_alias}>) -> {builder_type} {{\n Self {{ inner: Arc::new((*self.inner).clone().{field_name}({param_name}.as_ref().map(|v| &v.inner))) }}\n }}"
550 );
551 let new_method = format!(
552 " pub fn {field_name}(&self, {param_name}: &mut ext_php_rs::types::ZendObject) -> {builder_type} {{\n let bridge = {bridge_struct}::new({param_name});\n let handle: html_to_markdown_rs::visitor::VisitorHandle = std::rc::Rc::new(std::cell::RefCell::new(bridge));\n Self {{ inner: Arc::new((*self.inner).clone().{field_name}(Some(handle))) }}\n }}"
553 );
554
555 content = content.replace(&old_method, &new_method);
556 }
557 }
558
559 Ok(vec![GeneratedFile {
560 path: PathBuf::from(&output_dir).join("lib.rs"),
561 content,
562 generated_header: false,
563 }])
564 }
565
566 fn generate_public_api(
567 &self,
568 api: &ApiSurface,
569 config: &ResolvedCrateConfig,
570 ) -> anyhow::Result<Vec<GeneratedFile>> {
571 let extension_name = config.php_extension_name();
572 let class_name = extension_name.to_pascal_case();
573
574 let mut content = String::new();
576 content.push_str(&crate::template_env::render(
577 "php_file_header.jinja",
578 minijinja::Value::default(),
579 ));
580 content.push_str(&hash::header(CommentStyle::DoubleSlash));
581 content.push_str(&crate::template_env::render(
582 "php_declare_strict_types.jinja",
583 minijinja::Value::default(),
584 ));
585
586 let namespace = php_autoload_namespace(config);
588
589 content.push_str(&crate::template_env::render(
590 "php_namespace.jinja",
591 context! { namespace => &namespace },
592 ));
593 content.push_str(&crate::template_env::render(
594 "php_facade_class_declaration.jinja",
595 context! { class_name => &class_name },
596 ));
597
598 let bridge_param_names_pub: ahash::AHashSet<&str> = config
600 .trait_bridges
601 .iter()
602 .filter_map(|b| b.param_name.as_deref())
603 .collect();
604
605 let no_arg_constructor_types: AHashSet<String> = api
610 .types
611 .iter()
612 .filter(|t| t.fields.iter().all(|f| f.optional))
613 .map(|t| t.name.clone())
614 .collect();
615
616 for func in &api.functions {
618 let base_name = func.name.to_lower_camel_case();
623 let method_name = if func.is_async && !func.name.ends_with("_async") {
624 format!("{}Async", base_name)
625 } else {
626 base_name
627 };
628 let return_php_type = php_type(&func.return_type);
629
630 let visible_params: Vec<_> = func
632 .params
633 .iter()
634 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
635 .collect();
636
637 content.push_str(&crate::template_env::render(
639 "php_phpdoc_block_start.jinja",
640 minijinja::Value::default(),
641 ));
642 for line in func.doc.lines() {
643 if line.is_empty() {
644 content.push_str(&crate::template_env::render(
645 "php_phpdoc_empty_line.jinja",
646 minijinja::Value::default(),
647 ));
648 } else {
649 content.push_str(&crate::template_env::render(
650 "php_phpdoc_text_line.jinja",
651 context! { text => line },
652 ));
653 }
654 }
655 if func.doc.is_empty() {
656 content.push_str(&crate::template_env::render(
657 "php_phpdoc_text_line.jinja",
658 context! { text => &format!("{}.", method_name) },
659 ));
660 }
661 content.push_str(&crate::template_env::render(
662 "php_phpdoc_empty_line.jinja",
663 minijinja::Value::default(),
664 ));
665 for p in &visible_params {
666 let ptype = php_phpdoc_type(&p.ty);
667 let nullable_prefix = if p.optional { "?" } else { "" };
668 content.push_str(&crate::template_env::render(
669 "php_phpdoc_param_line.jinja",
670 context! {
671 nullable_prefix => nullable_prefix,
672 param_type => &ptype,
673 param_name => &p.name,
674 },
675 ));
676 }
677 let return_phpdoc = php_phpdoc_type(&func.return_type);
678 content.push_str(&crate::template_env::render(
679 "php_phpdoc_return_line.jinja",
680 context! { return_type => &return_phpdoc },
681 ));
682 if func.error_type.is_some() {
683 content.push_str(&crate::template_env::render(
684 "php_phpdoc_throws_line.jinja",
685 context! {
686 namespace => namespace.as_str(),
687 class_name => &class_name,
688 },
689 ));
690 }
691 content.push_str(&crate::template_env::render(
692 "php_phpdoc_block_end.jinja",
693 minijinja::Value::default(),
694 ));
695
696 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
707 if let TypeRef::Named(name) = &p.ty {
708 (name.ends_with("Config") || name.as_str() == "config")
709 && no_arg_constructor_types.contains(name.as_str())
710 } else {
711 false
712 }
713 };
714
715 let mut first_optional_idx = None;
716 for (idx, p) in visible_params.iter().enumerate() {
717 if p.optional || is_optional_config_param(p) {
718 first_optional_idx = Some(idx);
719 break;
720 }
721 }
722
723 content.push_str(&crate::template_env::render(
724 "php_method_signature_start.jinja",
725 context! { method_name => &method_name },
726 ));
727
728 let params: Vec<String> = visible_params
729 .iter()
730 .enumerate()
731 .map(|(idx, p)| {
732 let ptype = php_type(&p.ty);
733 let should_be_optional = p.optional
738 || is_optional_config_param(p)
739 || first_optional_idx.is_some_and(|first| idx >= first);
740 if should_be_optional {
741 format!("?{} ${} = null", ptype, p.name)
742 } else {
743 format!("{} ${}", ptype, p.name)
744 }
745 })
746 .collect();
747 content.push_str(¶ms.join(", "));
748 content.push_str(&crate::template_env::render(
749 "php_method_signature_end.jinja",
750 context! { return_type => &return_php_type },
751 ));
752 let ext_method_name = if func.is_async {
757 format!("{}_async", func.name).to_lower_camel_case()
758 } else {
759 func.name.to_lower_camel_case()
760 };
761 let is_void = matches!(&func.return_type, TypeRef::Unit);
762 let call_params = visible_params
770 .iter()
771 .enumerate()
772 .map(|(idx, p)| {
773 let should_be_optional = p.optional
774 || is_optional_config_param(p)
775 || first_optional_idx.is_some_and(|first| idx >= first);
776 if should_be_optional && is_optional_config_param(p) {
777 if let TypeRef::Named(type_name) = &p.ty {
778 return format!("${} ?? new {}()", p.name, type_name);
779 }
780 }
781 format!("${}", p.name)
782 })
783 .collect::<Vec<_>>()
784 .join(", ");
785 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
786 if is_void {
787 content.push_str(&crate::template_env::render(
788 "php_method_call_statement.jinja",
789 context! { call_expr => &call_expr },
790 ));
791 } else {
792 content.push_str(&crate::template_env::render(
793 "php_method_call_return.jinja",
794 context! { call_expr => &call_expr },
795 ));
796 }
797 content.push_str(&crate::template_env::render(
798 "php_method_end.jinja",
799 minijinja::Value::default(),
800 ));
801 }
802
803 content.push_str(&crate::template_env::render(
804 "php_class_end.jinja",
805 minijinja::Value::default(),
806 ));
807
808 let output_dir = config
812 .php
813 .as_ref()
814 .and_then(|p| p.stubs.as_ref())
815 .map(|s| s.output.to_string_lossy().to_string())
816 .unwrap_or_else(|| "packages/php/src/".to_string());
817
818 Ok(vec![GeneratedFile {
819 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
820 content,
821 generated_header: false,
822 }])
823 }
824
825 fn generate_type_stubs(
826 &self,
827 api: &ApiSurface,
828 config: &ResolvedCrateConfig,
829 ) -> anyhow::Result<Vec<GeneratedFile>> {
830 let extension_name = config.php_extension_name();
831 let class_name = extension_name.to_pascal_case();
832
833 let namespace = php_autoload_namespace(config);
835
836 let mut content = String::new();
841 content.push_str(&crate::template_env::render(
842 "php_file_header.jinja",
843 minijinja::Value::default(),
844 ));
845 content.push_str(&hash::header(CommentStyle::DoubleSlash));
846 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
847 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
848 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
849 content.push_str(&crate::template_env::render(
850 "php_declare_strict_types.jinja",
851 minijinja::Value::default(),
852 ));
853 content.push_str(&crate::template_env::render(
855 "php_namespace_block_begin.jinja",
856 context! { namespace => &namespace },
857 ));
858
859 content.push_str(&crate::template_env::render(
861 "php_exception_class_declaration.jinja",
862 context! { class_name => &class_name },
863 ));
864 content.push_str(
865 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
866 );
867 content.push_str("}\n\n");
868
869 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
871 if typ.is_opaque {
872 if !typ.doc.is_empty() {
873 content.push_str("/**\n");
874 for line in typ.doc.lines() {
875 if line.is_empty() {
876 content.push_str(" *\n");
877 } else {
878 content.push_str(&crate::template_env::render(
879 "php_phpdoc_doc_line.jinja",
880 context! { line => line },
881 ));
882 }
883 }
884 content.push_str(" */\n");
885 }
886 content.push_str(&crate::template_env::render(
887 "php_opaque_class_stub_declaration.jinja",
888 context! { class_name => &typ.name },
889 ));
890 content.push_str("}\n\n");
892 }
893 }
894
895 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
897 if typ.is_opaque || typ.fields.is_empty() {
898 continue;
899 }
900 if !typ.doc.is_empty() {
901 content.push_str("/**\n");
902 for line in typ.doc.lines() {
903 if line.is_empty() {
904 content.push_str(" *\n");
905 } else {
906 content.push_str(&crate::template_env::render(
907 "php_phpdoc_doc_line.jinja",
908 context! { line => line },
909 ));
910 }
911 }
912 content.push_str(" */\n");
913 }
914 content.push_str(&crate::template_env::render(
915 "php_record_class_stub_declaration.jinja",
916 context! { class_name => &typ.name },
917 ));
918
919 for field in &typ.fields {
921 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
922 let prop_type = if field.optional {
923 let inner = php_type(&field.ty);
924 if inner.starts_with('?') {
925 inner
926 } else {
927 format!("?{inner}")
928 }
929 } else {
930 php_type(&field.ty)
931 };
932 if is_array {
933 let phpdoc = php_phpdoc_type(&field.ty);
934 let nullable_prefix = if field.optional { "?" } else { "" };
935 content.push_str(&crate::template_env::render(
936 "php_property_type_annotation.jinja",
937 context! {
938 nullable_prefix => nullable_prefix,
939 phpdoc => &phpdoc,
940 },
941 ));
942 }
943 content.push_str(&crate::template_env::render(
944 "php_property_stub.jinja",
945 context! {
946 prop_type => &prop_type,
947 field_name => &field.name,
948 },
949 ));
950 }
951 content.push('\n');
952
953 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
957 sorted_fields.sort_by_key(|f| f.optional);
958
959 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
962 .iter()
963 .copied()
964 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
965 .collect();
966 if !array_fields.is_empty() {
967 content.push_str(" /**\n");
968 for f in &array_fields {
969 let phpdoc = php_phpdoc_type(&f.ty);
970 let nullable_prefix = if f.optional { "?" } else { "" };
971 content.push_str(&crate::template_env::render(
972 "php_phpdoc_array_param.jinja",
973 context! {
974 nullable_prefix => nullable_prefix,
975 phpdoc => &phpdoc,
976 param_name => &f.name,
977 },
978 ));
979 }
980 content.push_str(" */\n");
981 }
982
983 let params: Vec<String> = sorted_fields
984 .iter()
985 .map(|f| {
986 let ptype = php_type(&f.ty);
987 let nullable = if f.optional && !ptype.starts_with('?') {
988 format!("?{ptype}")
989 } else {
990 ptype
991 };
992 let default = if f.optional { " = null" } else { "" };
993 format!(" {} ${}{}", nullable, f.name, default)
994 })
995 .collect();
996 content.push_str(&crate::template_env::render(
997 "php_constructor_method.jinja",
998 context! { params => ¶ms.join(",\n") },
999 ));
1000
1001 for field in &typ.fields {
1003 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
1004 let return_type = if field.optional {
1005 let inner = php_type(&field.ty);
1006 if inner.starts_with('?') {
1007 inner
1008 } else {
1009 format!("?{inner}")
1010 }
1011 } else {
1012 php_type(&field.ty)
1013 };
1014 let getter_name = field.name.to_lower_camel_case();
1015 if is_array {
1017 let phpdoc = php_phpdoc_type(&field.ty);
1018 let nullable_prefix = if field.optional { "?" } else { "" };
1019 content.push_str(&crate::template_env::render(
1020 "php_constructor_doc_return.jinja",
1021 context! { return_type => &format!("{nullable_prefix}{phpdoc}") },
1022 ));
1023 }
1024 let is_void_getter = return_type == "void";
1025 let getter_body = if is_void_getter {
1026 "{ }".to_string()
1027 } else {
1028 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1029 };
1030 content.push_str(&crate::template_env::render(
1031 "php_getter_stub.jinja",
1032 context! {
1033 getter_name => &format!("get{}", getter_name.to_pascal_case()),
1034 return_type => &return_type,
1035 getter_body => &getter_body,
1036 },
1037 ));
1038 }
1039
1040 content.push_str("}\n\n");
1041 }
1042
1043 for enum_def in &api.enums {
1046 if is_tagged_data_enum(enum_def) {
1047 if !enum_def.doc.is_empty() {
1049 content.push_str("/**\n");
1050 for line in enum_def.doc.lines() {
1051 if line.is_empty() {
1052 content.push_str(" *\n");
1053 } else {
1054 content.push_str(&crate::template_env::render(
1055 "php_phpdoc_doc_line.jinja",
1056 context! { line => line },
1057 ));
1058 }
1059 }
1060 content.push_str(" */\n");
1061 }
1062 content.push_str(&crate::template_env::render(
1063 "php_record_class_stub_declaration.jinja",
1064 context! { class_name => &enum_def.name },
1065 ));
1066 content.push_str("}\n\n");
1067 } else {
1068 content.push_str(&crate::template_env::render(
1070 "php_tagged_enum_declaration.jinja",
1071 context! { enum_name => &enum_def.name },
1072 ));
1073 for variant in &enum_def.variants {
1074 let case_name = sanitize_php_enum_case(&variant.name);
1075 content.push_str(&crate::template_env::render(
1076 "php_enum_variant_stub.jinja",
1077 context! {
1078 variant_name => case_name,
1079 value => &variant.name,
1080 },
1081 ));
1082 }
1083 content.push_str("}\n\n");
1084 }
1085 }
1086
1087 if !api.functions.is_empty() {
1092 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1094 .trait_bridges
1095 .iter()
1096 .filter_map(|b| b.param_name.as_deref())
1097 .collect();
1098
1099 content.push_str(&crate::template_env::render(
1100 "php_api_class_declaration.jinja",
1101 context! { class_name => &class_name },
1102 ));
1103 for func in &api.functions {
1104 let return_type = php_type_fq(&func.return_type, &namespace);
1105 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1106 let visible_params: Vec<_> = func
1108 .params
1109 .iter()
1110 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1111 .collect();
1112 let has_array_params = visible_params
1119 .iter()
1120 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1121 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1122 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1123 if has_array_params || has_array_return {
1124 content.push_str(" /**\n");
1125 for p in &visible_params {
1126 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1127 let nullable_prefix = if p.optional { "?" } else { "" };
1128 content.push_str(&crate::template_env::render(
1129 "php_phpdoc_static_param.jinja",
1130 context! {
1131 nullable_prefix => nullable_prefix,
1132 ptype => &ptype,
1133 param_name => &p.name,
1134 },
1135 ));
1136 }
1137 content.push_str(&crate::template_env::render(
1138 "php_phpdoc_static_return.jinja",
1139 context! { return_phpdoc => &return_phpdoc },
1140 ));
1141 content.push_str(" */\n");
1142 }
1143 let params: Vec<String> = visible_params
1144 .iter()
1145 .map(|p| {
1146 let ptype = php_type_fq(&p.ty, &namespace);
1147 if p.optional {
1148 format!("?{} ${} = null", ptype, p.name)
1149 } else {
1150 format!("{} ${}", ptype, p.name)
1151 }
1152 })
1153 .collect();
1154 let stub_method_name = if func.is_async {
1156 format!("{}_async", func.name).to_lower_camel_case()
1157 } else {
1158 func.name.to_lower_camel_case()
1159 };
1160 let is_void_stub = return_type == "void";
1161 let stub_body = if is_void_stub {
1162 "{ }".to_string()
1163 } else {
1164 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1165 };
1166 content.push_str(&crate::template_env::render(
1167 "php_static_method_stub.jinja",
1168 context! {
1169 method_name => &stub_method_name,
1170 params => ¶ms.join(", "),
1171 return_type => &return_type,
1172 stub_body => &stub_body,
1173 },
1174 ));
1175 }
1176 content.push_str("}\n\n");
1177 }
1178
1179 content.push_str(&crate::template_env::render(
1181 "php_namespace_block_end.jinja",
1182 minijinja::Value::default(),
1183 ));
1184
1185 let output_dir = config
1187 .php
1188 .as_ref()
1189 .and_then(|p| p.stubs.as_ref())
1190 .map(|s| s.output.to_string_lossy().to_string())
1191 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1192
1193 Ok(vec![GeneratedFile {
1194 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1195 content,
1196 generated_header: false,
1197 }])
1198 }
1199
1200 fn build_config(&self) -> Option<BuildConfig> {
1201 Some(BuildConfig {
1202 tool: "cargo",
1203 crate_suffix: "-php",
1204 build_dep: BuildDependency::None,
1205 post_build: vec![],
1206 })
1207 }
1208}
1209
1210fn php_phpdoc_type(ty: &TypeRef) -> String {
1213 match ty {
1214 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1215 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1216 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1217 _ => php_type(ty),
1218 }
1219}
1220
1221fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1223 match ty {
1224 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1225 TypeRef::Map(k, v) => format!(
1226 "array<{}, {}>",
1227 php_phpdoc_type_fq(k, namespace),
1228 php_phpdoc_type_fq(v, namespace)
1229 ),
1230 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1231 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1232 _ => php_type(ty),
1233 }
1234}
1235
1236fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1238 match ty {
1239 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1240 TypeRef::Optional(inner) => {
1241 let inner_type = php_type_fq(inner, namespace);
1242 if inner_type.starts_with('?') {
1243 inner_type
1244 } else {
1245 format!("?{inner_type}")
1246 }
1247 }
1248 _ => php_type(ty),
1249 }
1250}
1251
1252fn php_type(ty: &TypeRef) -> String {
1254 match ty {
1255 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1256 TypeRef::Primitive(p) => match p {
1257 PrimitiveType::Bool => "bool".to_string(),
1258 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1259 PrimitiveType::U8
1260 | PrimitiveType::U16
1261 | PrimitiveType::U32
1262 | PrimitiveType::U64
1263 | PrimitiveType::I8
1264 | PrimitiveType::I16
1265 | PrimitiveType::I32
1266 | PrimitiveType::I64
1267 | PrimitiveType::Usize
1268 | PrimitiveType::Isize => "int".to_string(),
1269 },
1270 TypeRef::Optional(inner) => {
1271 let inner_type = php_type(inner);
1274 if inner_type.starts_with('?') {
1275 inner_type
1276 } else {
1277 format!("?{inner_type}")
1278 }
1279 }
1280 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1281 TypeRef::Named(name) => name.clone(),
1282 TypeRef::Unit => "void".to_string(),
1283 TypeRef::Duration => "float".to_string(),
1284 }
1285}