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, is_untagged_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 serializable_opaque_type_names: &[],
67 never_skip_cfg_field_names: &[],
68 }
69 }
70}
71
72impl Backend for PhpBackend {
73 fn name(&self) -> &str {
74 "php"
75 }
76
77 fn language(&self) -> Language {
78 Language::Php
79 }
80
81 fn capabilities(&self) -> Capabilities {
82 Capabilities {
83 supports_async: false,
84 supports_classes: true,
85 supports_enums: true,
86 supports_option: true,
87 supports_result: true,
88 ..Capabilities::default()
89 }
90 }
91
92 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
93 let data_enum_names: AHashSet<String> = api
96 .enums
97 .iter()
98 .filter(|e| is_tagged_data_enum(e))
99 .map(|e| e.name.clone())
100 .collect();
101 let untagged_data_enum_names: AHashSet<String> = api
102 .enums
103 .iter()
104 .filter(|e| is_untagged_data_enum(e))
105 .map(|e| e.name.clone())
106 .collect();
107 let enum_names: AHashSet<String> = api
110 .enums
111 .iter()
112 .filter(|e| !is_tagged_data_enum(e) && !is_untagged_data_enum(e))
113 .map(|e| e.name.clone())
114 .collect();
115 let mapper = PhpMapper {
116 enum_names: enum_names.clone(),
117 data_enum_names: data_enum_names.clone(),
118 untagged_data_enum_names: untagged_data_enum_names.clone(),
119 };
120 let core_import = config.core_import_name();
121
122 let php_config = config.php.as_ref();
124 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
125 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
126
127 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
128 let has_serde = detect_serde_available(&output_dir);
129
130 let bridge_type_aliases_php: Vec<String> = config
136 .trait_bridges
137 .iter()
138 .filter_map(|b| b.type_alias.clone())
139 .collect();
140 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
141 let mut opaque_names_vec_php: Vec<String> = api
142 .types
143 .iter()
144 .filter(|t| t.is_opaque)
145 .map(|t| t.name.clone())
146 .collect();
147 opaque_names_vec_php.extend(bridge_type_aliases_php);
148
149 let mut cfg = Self::binding_config(&core_import, has_serde);
150 cfg.opaque_type_names = &opaque_names_vec_php;
151 let never_skip_cfg_field_names: Vec<String> = config
152 .trait_bridges
153 .iter()
154 .filter_map(|b| {
155 if b.bind_via == alef_core::config::BridgeBinding::OptionsField {
156 b.resolved_options_field().map(|s| s.to_string())
157 } else {
158 None
159 }
160 })
161 .collect();
162 cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;
163
164 let mut builder = RustFileBuilder::new().with_generated_header();
166 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
167 builder.add_inner_attribute("allow(unsafe_code)");
168 builder.add_inner_attribute("allow(non_snake_case)");
170 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)");
171 builder.add_import("ext_php_rs::prelude::*");
172
173 if has_serde {
175 builder.add_import("serde_json");
176 }
177
178 for trait_path in generators::collect_trait_imports(api) {
180 builder.add_import(&trait_path);
181 }
182
183 let has_maps = api.types.iter().any(|t| {
185 t.fields
186 .iter()
187 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
188 }) || api
189 .functions
190 .iter()
191 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
192 if has_maps {
193 builder.add_import("std::collections::HashMap");
194 }
195
196 builder.add_item(
201 "#[derive(Debug, Clone, Default)]\n\
202 pub struct PhpBytes(pub Vec<u8>);\n\
203 \n\
204 impl<'a> ext_php_rs::convert::FromZval<'a> for PhpBytes {\n \
205 const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::String;\n \
206 fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {\n \
207 zval.zend_str().map(|zs| PhpBytes(zs.as_bytes().to_vec()))\n \
208 }\n\
209 }\n\
210 \n\
211 impl From<PhpBytes> for Vec<u8> {\n \
212 fn from(b: PhpBytes) -> Self { b.0 }\n\
213 }\n\
214 \n\
215 impl From<Vec<u8>> for PhpBytes {\n \
216 fn from(v: Vec<u8>) -> Self { PhpBytes(v) }\n\
217 }\n",
218 );
219
220 let custom_mods = config.custom_modules.for_language(Language::Php);
222 for module in custom_mods {
223 builder.add_item(&format!("pub mod {module};"));
224 }
225
226 let has_async =
228 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
229
230 if has_async {
231 builder.add_item(&gen_tokio_runtime());
232 }
233
234 let opaque_types: AHashSet<String> = api
236 .types
237 .iter()
238 .filter(|t| t.is_opaque)
239 .map(|t| t.name.clone())
240 .collect();
241 if !opaque_types.is_empty() {
242 builder.add_import("std::sync::Arc");
243 }
244
245 let extension_name = config.php_extension_name();
248 let php_namespace = php_autoload_namespace(config);
249
250 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
252
253 for adapter in &config.adapters {
255 match adapter.pattern {
256 alef_core::config::AdapterPattern::Streaming => {
257 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
258 if let Some(struct_code) = adapter_bodies.get(&key) {
259 builder.add_item(struct_code);
260 }
261 }
262 alef_core::config::AdapterPattern::CallbackBridge => {
263 let struct_key = format!("{}.__bridge_struct__", adapter.name);
264 let impl_key = format!("{}.__bridge_impl__", adapter.name);
265 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
266 builder.add_item(struct_code);
267 }
268 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
269 builder.add_item(impl_code);
270 }
271 }
272 _ => {}
273 }
274 }
275
276 for typ in api
277 .types
278 .iter()
279 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
280 {
281 if typ.is_opaque {
282 let ns_escaped = php_namespace.replace('\\', "\\\\");
286 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
287 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
288 let opaque_cfg = RustBindingConfig {
289 struct_attrs: &opaque_attr_arr,
290 ..cfg
291 };
292 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
293 builder.add_item(&gen_opaque_struct_methods(
294 typ,
295 &mapper,
296 &opaque_types,
297 &core_import,
298 &adapter_bodies,
299 ));
300 } else {
301 builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace), &enum_names));
304 builder.add_item(&types::gen_struct_methods_with_exclude(
305 typ,
306 &mapper,
307 has_serde,
308 &core_import,
309 &opaque_types,
310 &enum_names,
311 &api.enums,
312 &exclude_functions,
313 &bridge_type_aliases_set,
314 ));
315 }
316 }
317
318 for enum_def in &api.enums {
319 if is_tagged_data_enum(enum_def) {
320 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
322 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
323 } else {
324 builder.add_item(&gen_enum_constants(enum_def));
325 }
326 }
327
328 let included_functions: Vec<_> = api
333 .functions
334 .iter()
335 .filter(|f| !exclude_functions.contains(&f.name))
336 .collect();
337 if !included_functions.is_empty() {
338 let facade_class_name = extension_name.to_pascal_case();
339 let mut method_items: Vec<String> = Vec::new();
342 for func in included_functions {
343 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
344 if let Some((param_idx, bridge_cfg)) = bridge_param {
345 method_items.push(crate::trait_bridge::gen_bridge_function(
346 func,
347 param_idx,
348 bridge_cfg,
349 &mapper,
350 &opaque_types,
351 &core_import,
352 ));
353 } else if func.is_async {
354 method_items.push(gen_async_function_as_static_method(
355 func,
356 &mapper,
357 &opaque_types,
358 &core_import,
359 &config.trait_bridges,
360 ));
361 } else {
362 method_items.push(gen_function_as_static_method(
363 func,
364 &mapper,
365 &opaque_types,
366 &core_import,
367 &config.trait_bridges,
368 has_serde,
369 ));
370 }
371 }
372
373 let methods_joined = method_items
374 .iter()
375 .map(|m| {
376 m.lines()
378 .map(|l| {
379 if l.is_empty() {
380 String::new()
381 } else {
382 format!(" {l}")
383 }
384 })
385 .collect::<Vec<_>>()
386 .join("\n")
387 })
388 .collect::<Vec<_>>()
389 .join("\n\n");
390 let php_api_class_name = format!("{facade_class_name}Api");
393 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
395 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
396 let facade_struct = format!(
397 "#[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}}"
398 );
399 builder.add_item(&facade_struct);
400
401 for bridge_cfg in &config.trait_bridges {
403 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
404 let bridge = crate::trait_bridge::gen_trait_bridge(
405 trait_type,
406 bridge_cfg,
407 &core_import,
408 &config.error_type_name(),
409 &config.error_constructor_expr(),
410 api,
411 );
412 for imp in &bridge.imports {
413 builder.add_import(imp);
414 }
415 builder.add_item(&bridge.code);
416 }
417 }
418 }
419
420 let convertible = alef_codegen::conversions::convertible_types(api);
421 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
422 let input_types = alef_codegen::conversions::input_type_names(api);
423 let enum_names_ref = &mapper.enum_names;
428 let bridge_skip_types: Vec<String> = config
429 .trait_bridges
430 .iter()
431 .filter_map(|b| b.type_alias.clone())
432 .collect();
433 let php_conv_config = ConversionConfig {
434 cast_large_ints_to_i64: true,
435 enum_string_names: Some(enum_names_ref),
436 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
437 json_as_value: true,
441 include_cfg_metadata: false,
442 option_duration_on_defaults: true,
443 from_binding_skip_types: &bridge_skip_types,
444 never_skip_cfg_field_names: &never_skip_cfg_field_names,
445 ..Default::default()
446 };
447 let mut enum_tainted: AHashSet<String> = AHashSet::new();
449 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
450 if has_enum_named_field(typ, enum_names_ref) {
451 enum_tainted.insert(typ.name.clone());
452 }
453 }
454 let mut changed = true;
456 while changed {
457 changed = false;
458 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
459 if !enum_tainted.contains(&typ.name)
460 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
461 {
462 enum_tainted.insert(typ.name.clone());
463 changed = true;
464 }
465 }
466 }
467 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
468 if input_types.contains(&typ.name)
470 && !enum_tainted.contains(&typ.name)
471 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
472 {
473 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
474 typ,
475 &core_import,
476 &php_conv_config,
477 ));
478 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
479 builder.add_item(&gen_enum_tainted_from_binding_to_core(
486 typ,
487 &core_import,
488 enum_names_ref,
489 &enum_tainted,
490 &php_conv_config,
491 &api.enums,
492 &bridge_type_aliases_set,
493 ));
494 }
495 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
497 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
498 typ,
499 &core_import,
500 &opaque_types,
501 &php_conv_config,
502 ));
503 }
504 }
505
506 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
508 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
509 }
510
511 for error in &api.errors {
513 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
514 }
515
516 if has_serde {
520 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
521 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
522 pub fn max_compression_ratio() -> i64 { 100 }\n\
523 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
524 pub fn max_nesting_depth() -> i64 { 1024 }\n\
525 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
526 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
527 pub fn max_iterations() -> i64 { 10_000_000 }\n\
528 pub fn max_xml_depth() -> i64 { 1024 }\n\
529 pub fn max_table_cells() -> i64 { 100_000 }\n\
530 }";
531 builder.add_item(serde_module);
532 }
533
534 let php_config = config.php.as_ref();
540 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
541
542 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
546 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
547 }
548
549 let mut class_registrations = String::new();
552 for typ in api
553 .types
554 .iter()
555 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
556 {
557 class_registrations.push_str(&crate::template_env::render(
558 "php_class_registration.jinja",
559 context! { class_name => &typ.name },
560 ));
561 }
562 if !api.functions.is_empty() {
564 let facade_class_name = extension_name.to_pascal_case();
565 class_registrations.push_str(&crate::template_env::render(
566 "php_class_registration.jinja",
567 context! { class_name => &format!("{facade_class_name}Api") },
568 ));
569 }
570 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
573 class_registrations.push_str(&crate::template_env::render(
574 "php_class_registration.jinja",
575 context! { class_name => &enum_def.name },
576 ));
577 }
578 builder.add_item(&format!(
579 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
580 ));
581
582 let mut content = builder.build();
583
584 for bridge in &config.trait_bridges {
589 if let Some(field_name) = bridge.resolved_options_field() {
590 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
591 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
592 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
593 let builder_type = format!("{}Builder", options_type);
594 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
595
596 let old_method = format!(
602 " 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 }}"
603 );
604 let new_method = format!(
605 " 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 }}"
606 );
607
608 content = content.replace(&old_method, &new_method);
609 }
610 }
611
612 Ok(vec![GeneratedFile {
613 path: PathBuf::from(&output_dir).join("lib.rs"),
614 content,
615 generated_header: false,
616 }])
617 }
618
619 fn generate_public_api(
620 &self,
621 api: &ApiSurface,
622 config: &ResolvedCrateConfig,
623 ) -> anyhow::Result<Vec<GeneratedFile>> {
624 let extension_name = config.php_extension_name();
625 let class_name = extension_name.to_pascal_case();
626
627 let mut content = String::new();
629 content.push_str(&crate::template_env::render(
630 "php_file_header.jinja",
631 minijinja::Value::default(),
632 ));
633 content.push_str(&hash::header(CommentStyle::DoubleSlash));
634 content.push_str(&crate::template_env::render(
635 "php_declare_strict_types.jinja",
636 minijinja::Value::default(),
637 ));
638
639 let namespace = php_autoload_namespace(config);
641
642 content.push_str(&crate::template_env::render(
643 "php_namespace.jinja",
644 context! { namespace => &namespace },
645 ));
646 content.push_str(&crate::template_env::render(
647 "php_facade_class_declaration.jinja",
648 context! { class_name => &class_name },
649 ));
650
651 let bridge_param_names_pub: ahash::AHashSet<&str> = config
653 .trait_bridges
654 .iter()
655 .filter_map(|b| b.param_name.as_deref())
656 .collect();
657
658 let no_arg_constructor_types: AHashSet<String> = api
663 .types
664 .iter()
665 .filter(|t| t.fields.iter().all(|f| f.optional))
666 .map(|t| t.name.clone())
667 .collect();
668
669 for func in &api.functions {
671 let method_name = func.name.to_lower_camel_case();
676 let return_php_type = php_type(&func.return_type);
677
678 let visible_params: Vec<_> = func
680 .params
681 .iter()
682 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
683 .collect();
684
685 content.push_str(&crate::template_env::render(
687 "php_phpdoc_block_start.jinja",
688 minijinja::Value::default(),
689 ));
690 if func.doc.is_empty() {
691 content.push_str(&crate::template_env::render(
692 "php_phpdoc_text_line.jinja",
693 context! { text => &format!("{}.", method_name) },
694 ));
695 } else {
696 content.push_str(&crate::template_env::render(
697 "php_phpdoc_lines.jinja",
698 context! {
699 doc_lines => func.doc.lines().collect::<Vec<_>>(),
700 indent => " ",
701 },
702 ));
703 }
704 content.push_str(&crate::template_env::render(
705 "php_phpdoc_empty_line.jinja",
706 minijinja::Value::default(),
707 ));
708 for p in &visible_params {
709 let ptype = php_phpdoc_type(&p.ty);
710 let nullable_prefix = if p.optional { "?" } else { "" };
711 content.push_str(&crate::template_env::render(
712 "php_phpdoc_param_line.jinja",
713 context! {
714 nullable_prefix => nullable_prefix,
715 param_type => &ptype,
716 param_name => &p.name,
717 },
718 ));
719 }
720 let return_phpdoc = php_phpdoc_type(&func.return_type);
721 content.push_str(&crate::template_env::render(
722 "php_phpdoc_return_line.jinja",
723 context! { return_type => &return_phpdoc },
724 ));
725 if func.error_type.is_some() {
726 content.push_str(&crate::template_env::render(
727 "php_phpdoc_throws_line.jinja",
728 context! {
729 namespace => namespace.as_str(),
730 class_name => &class_name,
731 },
732 ));
733 }
734 content.push_str(&crate::template_env::render(
735 "php_phpdoc_block_end.jinja",
736 minijinja::Value::default(),
737 ));
738
739 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
750 if let TypeRef::Named(name) = &p.ty {
751 (name.ends_with("Config") || name.as_str() == "config")
752 && no_arg_constructor_types.contains(name.as_str())
753 } else {
754 false
755 }
756 };
757
758 let mut first_optional_idx = None;
759 for (idx, p) in visible_params.iter().enumerate() {
760 if p.optional || is_optional_config_param(p) {
761 first_optional_idx = Some(idx);
762 break;
763 }
764 }
765
766 content.push_str(&crate::template_env::render(
767 "php_method_signature_start.jinja",
768 context! { method_name => &method_name },
769 ));
770
771 let params: Vec<String> = visible_params
772 .iter()
773 .enumerate()
774 .map(|(idx, p)| {
775 let ptype = php_type(&p.ty);
776 let should_be_optional = p.optional
781 || is_optional_config_param(p)
782 || first_optional_idx.is_some_and(|first| idx >= first);
783 if should_be_optional {
784 format!("?{} ${} = null", ptype, p.name)
785 } else {
786 format!("{} ${}", ptype, p.name)
787 }
788 })
789 .collect();
790 content.push_str(¶ms.join(", "));
791 content.push_str(&crate::template_env::render(
792 "php_method_signature_end.jinja",
793 context! { return_type => &return_php_type },
794 ));
795 let ext_method_name = if func.is_async {
800 format!("{}_async", func.name).to_lower_camel_case()
801 } else {
802 func.name.to_lower_camel_case()
803 };
804 let is_void = matches!(&func.return_type, TypeRef::Unit);
805 let call_params = visible_params
813 .iter()
814 .enumerate()
815 .map(|(idx, p)| {
816 let should_be_optional = p.optional
817 || is_optional_config_param(p)
818 || first_optional_idx.is_some_and(|first| idx >= first);
819 if should_be_optional && is_optional_config_param(p) {
820 if let TypeRef::Named(type_name) = &p.ty {
821 return format!("${} ?? new {}()", p.name, type_name);
822 }
823 }
824 format!("${}", p.name)
825 })
826 .collect::<Vec<_>>()
827 .join(", ");
828 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
829 if is_void {
830 content.push_str(&crate::template_env::render(
831 "php_method_call_statement.jinja",
832 context! { call_expr => &call_expr },
833 ));
834 } else {
835 content.push_str(&crate::template_env::render(
836 "php_method_call_return.jinja",
837 context! { call_expr => &call_expr },
838 ));
839 }
840 content.push_str(&crate::template_env::render(
841 "php_method_end.jinja",
842 minijinja::Value::default(),
843 ));
844 }
845
846 content.push_str(&crate::template_env::render(
847 "php_class_end.jinja",
848 minijinja::Value::default(),
849 ));
850
851 let output_dir = config
855 .php
856 .as_ref()
857 .and_then(|p| p.stubs.as_ref())
858 .map(|s| s.output.to_string_lossy().to_string())
859 .unwrap_or_else(|| "packages/php/src/".to_string());
860
861 Ok(vec![GeneratedFile {
862 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
863 content,
864 generated_header: false,
865 }])
866 }
867
868 fn generate_type_stubs(
869 &self,
870 api: &ApiSurface,
871 config: &ResolvedCrateConfig,
872 ) -> anyhow::Result<Vec<GeneratedFile>> {
873 let extension_name = config.php_extension_name();
874 let class_name = extension_name.to_pascal_case();
875
876 let namespace = php_autoload_namespace(config);
878
879 let mut content = String::new();
884 content.push_str(&crate::template_env::render(
885 "php_file_header.jinja",
886 minijinja::Value::default(),
887 ));
888 content.push_str(&hash::header(CommentStyle::DoubleSlash));
889 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
890 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
891 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
892 content.push_str(&crate::template_env::render(
893 "php_declare_strict_types.jinja",
894 minijinja::Value::default(),
895 ));
896 content.push_str(&crate::template_env::render(
898 "php_namespace_block_begin.jinja",
899 context! { namespace => &namespace },
900 ));
901
902 content.push_str(&crate::template_env::render(
904 "php_exception_class_declaration.jinja",
905 context! { class_name => &class_name },
906 ));
907 content.push_str(
908 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
909 );
910 content.push_str("}\n\n");
911
912 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
914 if typ.is_opaque {
915 if !typ.doc.is_empty() {
916 content.push_str("/**\n");
917 content.push_str(&crate::template_env::render(
918 "php_phpdoc_lines.jinja",
919 context! {
920 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
921 indent => "",
922 },
923 ));
924 content.push_str(" */\n");
925 }
926 content.push_str(&crate::template_env::render(
927 "php_opaque_class_stub_declaration.jinja",
928 context! { class_name => &typ.name },
929 ));
930 content.push_str("}\n\n");
932 }
933 }
934
935 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
937 if typ.is_opaque || typ.fields.is_empty() {
938 continue;
939 }
940 if !typ.doc.is_empty() {
941 content.push_str("/**\n");
942 content.push_str(&crate::template_env::render(
943 "php_phpdoc_lines.jinja",
944 context! {
945 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
946 indent => "",
947 },
948 ));
949 content.push_str(" */\n");
950 }
951 content.push_str(&crate::template_env::render(
952 "php_record_class_stub_declaration.jinja",
953 context! { class_name => &typ.name },
954 ));
955
956 for field in &typ.fields {
958 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
959 let prop_type = if field.optional {
960 let inner = php_type(&field.ty);
961 if inner.starts_with('?') {
962 inner
963 } else {
964 format!("?{inner}")
965 }
966 } else {
967 php_type(&field.ty)
968 };
969 if is_array {
970 let phpdoc = php_phpdoc_type(&field.ty);
971 let nullable_prefix = if field.optional { "?" } else { "" };
972 content.push_str(&crate::template_env::render(
973 "php_property_type_annotation.jinja",
974 context! {
975 nullable_prefix => nullable_prefix,
976 phpdoc => &phpdoc,
977 },
978 ));
979 }
980 content.push_str(&crate::template_env::render(
981 "php_property_stub.jinja",
982 context! {
983 prop_type => &prop_type,
984 field_name => &field.name,
985 },
986 ));
987 }
988 content.push('\n');
989
990 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
994 sorted_fields.sort_by_key(|f| f.optional);
995
996 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
999 .iter()
1000 .copied()
1001 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
1002 .collect();
1003 if !array_fields.is_empty() {
1004 content.push_str(" /**\n");
1005 for f in &array_fields {
1006 let phpdoc = php_phpdoc_type(&f.ty);
1007 let nullable_prefix = if f.optional { "?" } else { "" };
1008 content.push_str(&crate::template_env::render(
1009 "php_phpdoc_array_param.jinja",
1010 context! {
1011 nullable_prefix => nullable_prefix,
1012 phpdoc => &phpdoc,
1013 param_name => &f.name,
1014 },
1015 ));
1016 }
1017 content.push_str(" */\n");
1018 }
1019
1020 let params: Vec<String> = sorted_fields
1021 .iter()
1022 .map(|f| {
1023 let ptype = php_type(&f.ty);
1024 let nullable = if f.optional && !ptype.starts_with('?') {
1025 format!("?{ptype}")
1026 } else {
1027 ptype
1028 };
1029 let default = if f.optional { " = null" } else { "" };
1030 format!(" {} ${}{}", nullable, f.name, default)
1031 })
1032 .collect();
1033 content.push_str(&crate::template_env::render(
1034 "php_constructor_method.jinja",
1035 context! { params => ¶ms.join(",\n") },
1036 ));
1037
1038 for field in &typ.fields {
1040 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
1041 let return_type = if field.optional {
1042 let inner = php_type(&field.ty);
1043 if inner.starts_with('?') {
1044 inner
1045 } else {
1046 format!("?{inner}")
1047 }
1048 } else {
1049 php_type(&field.ty)
1050 };
1051 let getter_name = field.name.to_lower_camel_case();
1052 if is_array {
1054 let phpdoc = php_phpdoc_type(&field.ty);
1055 let nullable_prefix = if field.optional { "?" } else { "" };
1056 content.push_str(&crate::template_env::render(
1057 "php_constructor_doc_return.jinja",
1058 context! { return_type => &format!("{nullable_prefix}{phpdoc}") },
1059 ));
1060 }
1061 let is_void_getter = return_type == "void";
1062 let getter_body = if is_void_getter {
1063 "{ }".to_string()
1064 } else {
1065 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1066 };
1067 content.push_str(&crate::template_env::render(
1068 "php_getter_stub.jinja",
1069 context! {
1070 getter_name => &format!("get{}", getter_name.to_pascal_case()),
1071 return_type => &return_type,
1072 getter_body => &getter_body,
1073 },
1074 ));
1075 }
1076
1077 content.push_str("}\n\n");
1078 }
1079
1080 for enum_def in &api.enums {
1083 if is_tagged_data_enum(enum_def) {
1084 if !enum_def.doc.is_empty() {
1086 content.push_str("/**\n");
1087 content.push_str(&crate::template_env::render(
1088 "php_phpdoc_lines.jinja",
1089 context! {
1090 doc_lines => enum_def.doc.lines().collect::<Vec<_>>(),
1091 indent => "",
1092 },
1093 ));
1094 content.push_str(" */\n");
1095 }
1096 content.push_str(&crate::template_env::render(
1097 "php_record_class_stub_declaration.jinja",
1098 context! { class_name => &enum_def.name },
1099 ));
1100 content.push_str("}\n\n");
1101 } else {
1102 content.push_str(&crate::template_env::render(
1104 "php_tagged_enum_declaration.jinja",
1105 context! { enum_name => &enum_def.name },
1106 ));
1107 for variant in &enum_def.variants {
1108 let case_name = sanitize_php_enum_case(&variant.name);
1109 content.push_str(&crate::template_env::render(
1110 "php_enum_variant_stub.jinja",
1111 context! {
1112 variant_name => case_name,
1113 value => &variant.name,
1114 },
1115 ));
1116 }
1117 content.push_str("}\n\n");
1118 }
1119 }
1120
1121 if !api.functions.is_empty() {
1126 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1128 .trait_bridges
1129 .iter()
1130 .filter_map(|b| b.param_name.as_deref())
1131 .collect();
1132
1133 content.push_str(&crate::template_env::render(
1134 "php_api_class_declaration.jinja",
1135 context! { class_name => &class_name },
1136 ));
1137 for func in &api.functions {
1138 let return_type = php_type_fq(&func.return_type, &namespace);
1139 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1140 let visible_params: Vec<_> = func
1142 .params
1143 .iter()
1144 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1145 .collect();
1146 let has_array_params = visible_params
1153 .iter()
1154 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1155 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1156 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1157 if has_array_params || has_array_return {
1158 content.push_str(" /**\n");
1159 for p in &visible_params {
1160 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1161 let nullable_prefix = if p.optional { "?" } else { "" };
1162 content.push_str(&crate::template_env::render(
1163 "php_phpdoc_static_param.jinja",
1164 context! {
1165 nullable_prefix => nullable_prefix,
1166 ptype => &ptype,
1167 param_name => &p.name,
1168 },
1169 ));
1170 }
1171 content.push_str(&crate::template_env::render(
1172 "php_phpdoc_static_return.jinja",
1173 context! { return_phpdoc => &return_phpdoc },
1174 ));
1175 content.push_str(" */\n");
1176 }
1177 let params: Vec<String> = visible_params
1178 .iter()
1179 .map(|p| {
1180 let ptype = php_type_fq(&p.ty, &namespace);
1181 if p.optional {
1182 format!("?{} ${} = null", ptype, p.name)
1183 } else {
1184 format!("{} ${}", ptype, p.name)
1185 }
1186 })
1187 .collect();
1188 let stub_method_name = if func.is_async {
1190 format!("{}_async", func.name).to_lower_camel_case()
1191 } else {
1192 func.name.to_lower_camel_case()
1193 };
1194 let is_void_stub = return_type == "void";
1195 let stub_body = if is_void_stub {
1196 "{ }".to_string()
1197 } else {
1198 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1199 };
1200 content.push_str(&crate::template_env::render(
1201 "php_static_method_stub.jinja",
1202 context! {
1203 method_name => &stub_method_name,
1204 params => ¶ms.join(", "),
1205 return_type => &return_type,
1206 stub_body => &stub_body,
1207 },
1208 ));
1209 }
1210 content.push_str("}\n\n");
1211 }
1212
1213 content.push_str(&crate::template_env::render(
1215 "php_namespace_block_end.jinja",
1216 minijinja::Value::default(),
1217 ));
1218
1219 let output_dir = config
1221 .php
1222 .as_ref()
1223 .and_then(|p| p.stubs.as_ref())
1224 .map(|s| s.output.to_string_lossy().to_string())
1225 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1226
1227 Ok(vec![GeneratedFile {
1228 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1229 content,
1230 generated_header: false,
1231 }])
1232 }
1233
1234 fn build_config(&self) -> Option<BuildConfig> {
1235 Some(BuildConfig {
1236 tool: "cargo",
1237 crate_suffix: "-php",
1238 build_dep: BuildDependency::None,
1239 post_build: vec![],
1240 })
1241 }
1242}
1243
1244fn php_phpdoc_type(ty: &TypeRef) -> String {
1247 match ty {
1248 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1249 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1250 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1251 _ => php_type(ty),
1252 }
1253}
1254
1255fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1257 match ty {
1258 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1259 TypeRef::Map(k, v) => format!(
1260 "array<{}, {}>",
1261 php_phpdoc_type_fq(k, namespace),
1262 php_phpdoc_type_fq(v, namespace)
1263 ),
1264 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1265 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1266 _ => php_type(ty),
1267 }
1268}
1269
1270fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1272 match ty {
1273 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1274 TypeRef::Optional(inner) => {
1275 let inner_type = php_type_fq(inner, namespace);
1276 if inner_type.starts_with('?') {
1277 inner_type
1278 } else {
1279 format!("?{inner_type}")
1280 }
1281 }
1282 _ => php_type(ty),
1283 }
1284}
1285
1286fn php_type(ty: &TypeRef) -> String {
1288 match ty {
1289 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1290 TypeRef::Primitive(p) => match p {
1291 PrimitiveType::Bool => "bool".to_string(),
1292 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1293 PrimitiveType::U8
1294 | PrimitiveType::U16
1295 | PrimitiveType::U32
1296 | PrimitiveType::U64
1297 | PrimitiveType::I8
1298 | PrimitiveType::I16
1299 | PrimitiveType::I32
1300 | PrimitiveType::I64
1301 | PrimitiveType::Usize
1302 | PrimitiveType::Isize => "int".to_string(),
1303 },
1304 TypeRef::Optional(inner) => {
1305 let inner_type = php_type(inner);
1308 if inner_type.starts_with('?') {
1309 inner_type
1310 } else {
1311 format!("?{inner_type}")
1312 }
1313 }
1314 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1315 TypeRef::Named(name) => name.clone(),
1316 TypeRef::Unit => "void".to_string(),
1317 TypeRef::Duration => "float".to_string(),
1318 }
1319}