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 let lang_rename_all = config.serde_rename_all_for_language(Language::Php);
122
123 let php_config = config.php.as_ref();
125 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
126 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
127
128 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
129 let has_serde = detect_serde_available(&output_dir);
130
131 let bridge_type_aliases_php: Vec<String> = config
137 .trait_bridges
138 .iter()
139 .filter_map(|b| b.type_alias.clone())
140 .collect();
141 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
142 let mut opaque_names_vec_php: Vec<String> = api
143 .types
144 .iter()
145 .filter(|t| t.is_opaque)
146 .map(|t| t.name.clone())
147 .collect();
148 opaque_names_vec_php.extend(bridge_type_aliases_php);
149
150 let mut cfg = Self::binding_config(&core_import, has_serde);
151 cfg.opaque_type_names = &opaque_names_vec_php;
152 let never_skip_cfg_field_names: Vec<String> = config
153 .trait_bridges
154 .iter()
155 .filter_map(|b| {
156 if b.bind_via == alef_core::config::BridgeBinding::OptionsField {
157 b.resolved_options_field().map(|s| s.to_string())
158 } else {
159 None
160 }
161 })
162 .collect();
163 cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;
164
165 let mut builder = RustFileBuilder::new().with_generated_header();
167 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
168 builder.add_inner_attribute("allow(unsafe_code)");
169 builder.add_inner_attribute("allow(non_snake_case)");
171 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)");
172 builder.add_import("ext_php_rs::prelude::*");
173
174 if has_serde {
176 builder.add_import("serde_json");
177 }
178
179 for trait_path in generators::collect_trait_imports(api) {
181 builder.add_import(&trait_path);
182 }
183
184 let has_maps = api.types.iter().any(|t| {
186 t.fields
187 .iter()
188 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
189 }) || api
190 .functions
191 .iter()
192 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
193 if has_maps {
194 builder.add_import("std::collections::HashMap");
195 }
196
197 builder.add_item(
202 "#[derive(Debug, Clone, Default)]\n\
203 pub struct PhpBytes(pub Vec<u8>);\n\
204 \n\
205 impl<'a> ext_php_rs::convert::FromZval<'a> for PhpBytes {\n \
206 const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::String;\n \
207 fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {\n \
208 zval.zend_str().map(|zs| PhpBytes(zs.as_bytes().to_vec()))\n \
209 }\n\
210 }\n\
211 \n\
212 impl From<PhpBytes> for Vec<u8> {\n \
213 fn from(b: PhpBytes) -> Self { b.0 }\n\
214 }\n\
215 \n\
216 impl From<Vec<u8>> for PhpBytes {\n \
217 fn from(v: Vec<u8>) -> Self { PhpBytes(v) }\n\
218 }\n",
219 );
220
221 let custom_mods = config.custom_modules.for_language(Language::Php);
223 for module in custom_mods {
224 builder.add_item(&format!("pub mod {module};"));
225 }
226
227 let has_async =
229 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
230
231 if has_async {
232 builder.add_item(&gen_tokio_runtime());
233 }
234
235 let opaque_types: AHashSet<String> = api
237 .types
238 .iter()
239 .filter(|t| t.is_opaque)
240 .map(|t| t.name.clone())
241 .collect();
242 if !opaque_types.is_empty() {
243 builder.add_import("std::sync::Arc");
244 }
245
246 let extension_name = config.php_extension_name();
249 let php_namespace = php_autoload_namespace(config);
250
251 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
253
254 for adapter in &config.adapters {
256 match adapter.pattern {
257 alef_core::config::AdapterPattern::Streaming => {
258 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
259 if let Some(struct_code) = adapter_bodies.get(&key) {
260 builder.add_item(struct_code);
261 }
262 }
263 alef_core::config::AdapterPattern::CallbackBridge => {
264 let struct_key = format!("{}.__bridge_struct__", adapter.name);
265 let impl_key = format!("{}.__bridge_impl__", adapter.name);
266 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
267 builder.add_item(struct_code);
268 }
269 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
270 builder.add_item(impl_code);
271 }
272 }
273 _ => {}
274 }
275 }
276
277 for typ in api
278 .types
279 .iter()
280 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
281 {
282 if typ.is_opaque {
283 let ns_escaped = php_namespace.replace('\\', "\\\\");
287 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
288 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
289 let opaque_cfg = RustBindingConfig {
290 struct_attrs: &opaque_attr_arr,
291 ..cfg
292 };
293 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
294 builder.add_item(&gen_opaque_struct_methods(
295 typ,
296 &mapper,
297 &opaque_types,
298 &core_import,
299 &adapter_bodies,
300 ));
301 } else {
302 builder.add_item(&gen_php_struct(
305 typ,
306 &mapper,
307 &cfg,
308 Some(&php_namespace),
309 &enum_names,
310 &lang_rename_all,
311 ));
312 builder.add_item(&types::gen_struct_methods_with_exclude(
313 typ,
314 &mapper,
315 has_serde,
316 &core_import,
317 &opaque_types,
318 &enum_names,
319 &api.enums,
320 &exclude_functions,
321 &bridge_type_aliases_set,
322 &never_skip_cfg_field_names,
323 ));
324 }
325 }
326
327 for enum_def in &api.enums {
328 if is_tagged_data_enum(enum_def) {
329 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
331 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
332 } else {
333 builder.add_item(&gen_enum_constants(enum_def));
334 }
335 }
336
337 let included_functions: Vec<_> = api
342 .functions
343 .iter()
344 .filter(|f| !exclude_functions.contains(&f.name))
345 .collect();
346 if !included_functions.is_empty() {
347 let facade_class_name = extension_name.to_pascal_case();
348 let mut method_items: Vec<String> = Vec::new();
351 for func in included_functions {
352 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
353 if let Some((param_idx, bridge_cfg)) = bridge_param {
354 method_items.push(crate::trait_bridge::gen_bridge_function(
355 func,
356 param_idx,
357 bridge_cfg,
358 &mapper,
359 &opaque_types,
360 &core_import,
361 ));
362 } else if func.is_async {
363 method_items.push(gen_async_function_as_static_method(
364 func,
365 &mapper,
366 &opaque_types,
367 &core_import,
368 &config.trait_bridges,
369 ));
370 } else {
371 method_items.push(gen_function_as_static_method(
372 func,
373 &mapper,
374 &opaque_types,
375 &core_import,
376 &config.trait_bridges,
377 has_serde,
378 ));
379 }
380 }
381
382 let methods_joined = method_items
383 .iter()
384 .map(|m| {
385 m.lines()
387 .map(|l| {
388 if l.is_empty() {
389 String::new()
390 } else {
391 format!(" {l}")
392 }
393 })
394 .collect::<Vec<_>>()
395 .join("\n")
396 })
397 .collect::<Vec<_>>()
398 .join("\n\n");
399 let php_api_class_name = format!("{facade_class_name}Api");
402 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
404 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
405 let facade_struct = format!(
406 "#[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}}"
407 );
408 builder.add_item(&facade_struct);
409
410 for bridge_cfg in &config.trait_bridges {
412 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
413 let bridge = crate::trait_bridge::gen_trait_bridge(
414 trait_type,
415 bridge_cfg,
416 &core_import,
417 &config.error_type_name(),
418 &config.error_constructor_expr(),
419 api,
420 );
421 for imp in &bridge.imports {
422 builder.add_import(imp);
423 }
424 builder.add_item(&bridge.code);
425 }
426 }
427 }
428
429 let convertible = alef_codegen::conversions::convertible_types(api);
430 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
431 let input_types = alef_codegen::conversions::input_type_names(api);
432 let enum_names_ref = &mapper.enum_names;
437 let bridge_skip_types: Vec<String> = config
438 .trait_bridges
439 .iter()
440 .filter_map(|b| b.type_alias.clone())
441 .collect();
442 let php_conv_config = ConversionConfig {
443 cast_large_ints_to_i64: true,
444 enum_string_names: Some(enum_names_ref),
445 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
446 json_as_value: true,
450 include_cfg_metadata: false,
451 option_duration_on_defaults: true,
452 from_binding_skip_types: &bridge_skip_types,
453 never_skip_cfg_field_names: &never_skip_cfg_field_names,
454 ..Default::default()
455 };
456 let mut enum_tainted: AHashSet<String> = AHashSet::new();
458 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
459 if has_enum_named_field(typ, enum_names_ref) {
460 enum_tainted.insert(typ.name.clone());
461 }
462 }
463 let mut changed = true;
465 while changed {
466 changed = false;
467 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
468 if !enum_tainted.contains(&typ.name)
469 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
470 {
471 enum_tainted.insert(typ.name.clone());
472 changed = true;
473 }
474 }
475 }
476 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
477 if input_types.contains(&typ.name)
479 && !enum_tainted.contains(&typ.name)
480 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
481 {
482 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
483 typ,
484 &core_import,
485 &php_conv_config,
486 ));
487 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
488 builder.add_item(&gen_enum_tainted_from_binding_to_core(
495 typ,
496 &core_import,
497 enum_names_ref,
498 &enum_tainted,
499 &php_conv_config,
500 &api.enums,
501 &bridge_type_aliases_set,
502 ));
503 }
504 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
506 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
507 typ,
508 &core_import,
509 &opaque_types,
510 &php_conv_config,
511 ));
512 }
513 }
514
515 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
517 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
518 }
519
520 for error in &api.errors {
522 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
523 }
524
525 if has_serde {
529 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
530 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
531 pub fn max_compression_ratio() -> i64 { 100 }\n\
532 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
533 pub fn max_nesting_depth() -> i64 { 1024 }\n\
534 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
535 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
536 pub fn max_iterations() -> i64 { 10_000_000 }\n\
537 pub fn max_xml_depth() -> i64 { 1024 }\n\
538 pub fn max_table_cells() -> i64 { 100_000 }\n\
539 }";
540 builder.add_item(serde_module);
541 }
542
543 let php_config = config.php.as_ref();
549 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
550
551 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
555 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
556 }
557
558 let mut class_registrations = String::new();
561 for typ in api
562 .types
563 .iter()
564 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
565 {
566 class_registrations.push_str(&crate::template_env::render(
567 "php_class_registration.jinja",
568 context! { class_name => &typ.name },
569 ));
570 }
571 if !api.functions.is_empty() {
573 let facade_class_name = extension_name.to_pascal_case();
574 class_registrations.push_str(&crate::template_env::render(
575 "php_class_registration.jinja",
576 context! { class_name => &format!("{facade_class_name}Api") },
577 ));
578 }
579 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
582 class_registrations.push_str(&crate::template_env::render(
583 "php_class_registration.jinja",
584 context! { class_name => &enum_def.name },
585 ));
586 }
587 builder.add_item(&format!(
588 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
589 ));
590
591 let mut content = builder.build();
592
593 for bridge in &config.trait_bridges {
598 if let Some(field_name) = bridge.resolved_options_field() {
599 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
600 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
601 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
602 let builder_type = format!("{}Builder", options_type);
603 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
604
605 let old_method = format!(
611 " 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 }}"
612 );
613 let new_method = format!(
614 " 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 }}"
615 );
616
617 content = content.replace(&old_method, &new_method);
618 }
619 }
620
621 Ok(vec![GeneratedFile {
622 path: PathBuf::from(&output_dir).join("lib.rs"),
623 content,
624 generated_header: false,
625 }])
626 }
627
628 fn generate_public_api(
629 &self,
630 api: &ApiSurface,
631 config: &ResolvedCrateConfig,
632 ) -> anyhow::Result<Vec<GeneratedFile>> {
633 let extension_name = config.php_extension_name();
634 let class_name = extension_name.to_pascal_case();
635
636 let mut content = String::new();
638 content.push_str(&crate::template_env::render(
639 "php_file_header.jinja",
640 minijinja::Value::default(),
641 ));
642 content.push_str(&hash::header(CommentStyle::DoubleSlash));
643 content.push_str(&crate::template_env::render(
644 "php_declare_strict_types.jinja",
645 minijinja::Value::default(),
646 ));
647
648 let namespace = php_autoload_namespace(config);
650
651 content.push_str(&crate::template_env::render(
652 "php_namespace.jinja",
653 context! { namespace => &namespace },
654 ));
655 content.push_str(&crate::template_env::render(
656 "php_facade_class_declaration.jinja",
657 context! { class_name => &class_name },
658 ));
659
660 let bridge_param_names_pub: ahash::AHashSet<&str> = config
662 .trait_bridges
663 .iter()
664 .filter_map(|b| b.param_name.as_deref())
665 .collect();
666
667 let no_arg_constructor_types: AHashSet<String> = api
672 .types
673 .iter()
674 .filter(|t| t.fields.iter().all(|f| f.optional))
675 .map(|t| t.name.clone())
676 .collect();
677
678 for func in &api.functions {
680 let method_name = func.name.to_lower_camel_case();
685 let return_php_type = php_type(&func.return_type);
686
687 let visible_params: Vec<_> = func
689 .params
690 .iter()
691 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
692 .collect();
693
694 content.push_str(&crate::template_env::render(
696 "php_phpdoc_block_start.jinja",
697 minijinja::Value::default(),
698 ));
699 if func.doc.is_empty() {
700 content.push_str(&crate::template_env::render(
701 "php_phpdoc_text_line.jinja",
702 context! { text => &format!("{}.", method_name) },
703 ));
704 } else {
705 content.push_str(&crate::template_env::render(
706 "php_phpdoc_lines.jinja",
707 context! {
708 doc_lines => func.doc.lines().collect::<Vec<_>>(),
709 indent => " ",
710 },
711 ));
712 }
713 content.push_str(&crate::template_env::render(
714 "php_phpdoc_empty_line.jinja",
715 minijinja::Value::default(),
716 ));
717 for p in &visible_params {
718 let ptype = php_phpdoc_type(&p.ty);
719 let nullable_prefix = if p.optional { "?" } else { "" };
720 content.push_str(&crate::template_env::render(
721 "php_phpdoc_param_line.jinja",
722 context! {
723 nullable_prefix => nullable_prefix,
724 param_type => &ptype,
725 param_name => &p.name,
726 },
727 ));
728 }
729 let return_phpdoc = php_phpdoc_type(&func.return_type);
730 content.push_str(&crate::template_env::render(
731 "php_phpdoc_return_line.jinja",
732 context! { return_type => &return_phpdoc },
733 ));
734 if func.error_type.is_some() {
735 content.push_str(&crate::template_env::render(
736 "php_phpdoc_throws_line.jinja",
737 context! {
738 namespace => namespace.as_str(),
739 class_name => &class_name,
740 },
741 ));
742 }
743 content.push_str(&crate::template_env::render(
744 "php_phpdoc_block_end.jinja",
745 minijinja::Value::default(),
746 ));
747
748 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
759 if let TypeRef::Named(name) = &p.ty {
760 (name.ends_with("Config") || name.as_str() == "config")
761 && no_arg_constructor_types.contains(name.as_str())
762 } else {
763 false
764 }
765 };
766
767 let mut first_optional_idx = None;
768 for (idx, p) in visible_params.iter().enumerate() {
769 if p.optional || is_optional_config_param(p) {
770 first_optional_idx = Some(idx);
771 break;
772 }
773 }
774
775 content.push_str(&crate::template_env::render(
776 "php_method_signature_start.jinja",
777 context! { method_name => &method_name },
778 ));
779
780 let params: Vec<String> = visible_params
781 .iter()
782 .enumerate()
783 .map(|(idx, p)| {
784 let ptype = php_type(&p.ty);
785 let should_be_optional = p.optional
790 || is_optional_config_param(p)
791 || first_optional_idx.is_some_and(|first| idx >= first);
792 if should_be_optional {
793 format!("?{} ${} = null", ptype, p.name)
794 } else {
795 format!("{} ${}", ptype, p.name)
796 }
797 })
798 .collect();
799 content.push_str(¶ms.join(", "));
800 content.push_str(&crate::template_env::render(
801 "php_method_signature_end.jinja",
802 context! { return_type => &return_php_type },
803 ));
804 let ext_method_name = if func.is_async {
809 format!("{}_async", func.name).to_lower_camel_case()
810 } else {
811 func.name.to_lower_camel_case()
812 };
813 let is_void = matches!(&func.return_type, TypeRef::Unit);
814 let call_params = visible_params
822 .iter()
823 .enumerate()
824 .map(|(idx, p)| {
825 let should_be_optional = p.optional
826 || is_optional_config_param(p)
827 || first_optional_idx.is_some_and(|first| idx >= first);
828 if should_be_optional && is_optional_config_param(p) {
829 if let TypeRef::Named(type_name) = &p.ty {
830 return format!("${} ?? new {}()", p.name, type_name);
831 }
832 }
833 format!("${}", p.name)
834 })
835 .collect::<Vec<_>>()
836 .join(", ");
837 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
838 if is_void {
839 content.push_str(&crate::template_env::render(
840 "php_method_call_statement.jinja",
841 context! { call_expr => &call_expr },
842 ));
843 } else {
844 content.push_str(&crate::template_env::render(
845 "php_method_call_return.jinja",
846 context! { call_expr => &call_expr },
847 ));
848 }
849 content.push_str(&crate::template_env::render(
850 "php_method_end.jinja",
851 minijinja::Value::default(),
852 ));
853 }
854
855 content.push_str(&crate::template_env::render(
856 "php_class_end.jinja",
857 minijinja::Value::default(),
858 ));
859
860 let output_dir = config
864 .php
865 .as_ref()
866 .and_then(|p| p.stubs.as_ref())
867 .map(|s| s.output.to_string_lossy().to_string())
868 .unwrap_or_else(|| "packages/php/src/".to_string());
869
870 Ok(vec![GeneratedFile {
871 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
872 content,
873 generated_header: false,
874 }])
875 }
876
877 fn generate_type_stubs(
878 &self,
879 api: &ApiSurface,
880 config: &ResolvedCrateConfig,
881 ) -> anyhow::Result<Vec<GeneratedFile>> {
882 let extension_name = config.php_extension_name();
883 let class_name = extension_name.to_pascal_case();
884
885 let namespace = php_autoload_namespace(config);
887
888 let mut content = String::new();
893 content.push_str(&crate::template_env::render(
894 "php_file_header.jinja",
895 minijinja::Value::default(),
896 ));
897 content.push_str(&hash::header(CommentStyle::DoubleSlash));
898 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
899 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
900 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
901 content.push_str(&crate::template_env::render(
902 "php_declare_strict_types.jinja",
903 minijinja::Value::default(),
904 ));
905 content.push_str(&crate::template_env::render(
907 "php_namespace_block_begin.jinja",
908 context! { namespace => &namespace },
909 ));
910
911 content.push_str(&crate::template_env::render(
913 "php_exception_class_declaration.jinja",
914 context! { class_name => &class_name },
915 ));
916 content.push_str(
917 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
918 );
919 content.push_str("}\n\n");
920
921 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
923 if typ.is_opaque {
924 if !typ.doc.is_empty() {
925 content.push_str("/**\n");
926 content.push_str(&crate::template_env::render(
927 "php_phpdoc_lines.jinja",
928 context! {
929 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
930 indent => "",
931 },
932 ));
933 content.push_str(" */\n");
934 }
935 content.push_str(&crate::template_env::render(
936 "php_opaque_class_stub_declaration.jinja",
937 context! { class_name => &typ.name },
938 ));
939 content.push_str("}\n\n");
941 }
942 }
943
944 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
946 if typ.is_opaque || typ.fields.is_empty() {
947 continue;
948 }
949 if !typ.doc.is_empty() {
950 content.push_str("/**\n");
951 content.push_str(&crate::template_env::render(
952 "php_phpdoc_lines.jinja",
953 context! {
954 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
955 indent => "",
956 },
957 ));
958 content.push_str(" */\n");
959 }
960 content.push_str(&crate::template_env::render(
961 "php_record_class_stub_declaration.jinja",
962 context! { class_name => &typ.name },
963 ));
964
965 for field in &typ.fields {
967 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
968 let prop_type = if field.optional {
969 let inner = php_type(&field.ty);
970 if inner.starts_with('?') {
971 inner
972 } else {
973 format!("?{inner}")
974 }
975 } else {
976 php_type(&field.ty)
977 };
978 if is_array {
979 let phpdoc = php_phpdoc_type(&field.ty);
980 let nullable_prefix = if field.optional { "?" } else { "" };
981 content.push_str(&crate::template_env::render(
982 "php_property_type_annotation.jinja",
983 context! {
984 nullable_prefix => nullable_prefix,
985 phpdoc => &phpdoc,
986 },
987 ));
988 }
989 content.push_str(&crate::template_env::render(
990 "php_property_stub.jinja",
991 context! {
992 prop_type => &prop_type,
993 field_name => &field.name,
994 },
995 ));
996 }
997 content.push('\n');
998
999 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
1003 sorted_fields.sort_by_key(|f| f.optional);
1004
1005 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
1008 .iter()
1009 .copied()
1010 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
1011 .collect();
1012 if !array_fields.is_empty() {
1013 content.push_str(" /**\n");
1014 for f in &array_fields {
1015 let phpdoc = php_phpdoc_type(&f.ty);
1016 let nullable_prefix = if f.optional { "?" } else { "" };
1017 content.push_str(&crate::template_env::render(
1018 "php_phpdoc_array_param.jinja",
1019 context! {
1020 nullable_prefix => nullable_prefix,
1021 phpdoc => &phpdoc,
1022 param_name => &f.name,
1023 },
1024 ));
1025 }
1026 content.push_str(" */\n");
1027 }
1028
1029 let params: Vec<String> = sorted_fields
1030 .iter()
1031 .map(|f| {
1032 let ptype = php_type(&f.ty);
1033 let nullable = if f.optional && !ptype.starts_with('?') {
1034 format!("?{ptype}")
1035 } else {
1036 ptype
1037 };
1038 let default = if f.optional { " = null" } else { "" };
1039 format!(" {} ${}{}", nullable, f.name, default)
1040 })
1041 .collect();
1042 content.push_str(&crate::template_env::render(
1043 "php_constructor_method.jinja",
1044 context! { params => ¶ms.join(",\n") },
1045 ));
1046
1047 for field in &typ.fields {
1049 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
1050 let return_type = if field.optional {
1051 let inner = php_type(&field.ty);
1052 if inner.starts_with('?') {
1053 inner
1054 } else {
1055 format!("?{inner}")
1056 }
1057 } else {
1058 php_type(&field.ty)
1059 };
1060 let getter_name = field.name.to_lower_camel_case();
1061 if is_array {
1063 let phpdoc = php_phpdoc_type(&field.ty);
1064 let nullable_prefix = if field.optional { "?" } else { "" };
1065 content.push_str(&crate::template_env::render(
1066 "php_constructor_doc_return.jinja",
1067 context! { return_type => &format!("{nullable_prefix}{phpdoc}") },
1068 ));
1069 }
1070 let is_void_getter = return_type == "void";
1071 let getter_body = if is_void_getter {
1072 "{ }".to_string()
1073 } else {
1074 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1075 };
1076 content.push_str(&crate::template_env::render(
1077 "php_getter_stub.jinja",
1078 context! {
1079 getter_name => &format!("get{}", getter_name.to_pascal_case()),
1080 return_type => &return_type,
1081 getter_body => &getter_body,
1082 },
1083 ));
1084 }
1085
1086 content.push_str("}\n\n");
1087 }
1088
1089 for enum_def in &api.enums {
1092 if is_tagged_data_enum(enum_def) {
1093 if !enum_def.doc.is_empty() {
1095 content.push_str("/**\n");
1096 content.push_str(&crate::template_env::render(
1097 "php_phpdoc_lines.jinja",
1098 context! {
1099 doc_lines => enum_def.doc.lines().collect::<Vec<_>>(),
1100 indent => "",
1101 },
1102 ));
1103 content.push_str(" */\n");
1104 }
1105 content.push_str(&crate::template_env::render(
1106 "php_record_class_stub_declaration.jinja",
1107 context! { class_name => &enum_def.name },
1108 ));
1109 content.push_str("}\n\n");
1110 } else {
1111 content.push_str(&crate::template_env::render(
1113 "php_tagged_enum_declaration.jinja",
1114 context! { enum_name => &enum_def.name },
1115 ));
1116 for variant in &enum_def.variants {
1117 let case_name = sanitize_php_enum_case(&variant.name);
1118 content.push_str(&crate::template_env::render(
1119 "php_enum_variant_stub.jinja",
1120 context! {
1121 variant_name => case_name,
1122 value => &variant.name,
1123 },
1124 ));
1125 }
1126 content.push_str("}\n\n");
1127 }
1128 }
1129
1130 if !api.functions.is_empty() {
1135 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1137 .trait_bridges
1138 .iter()
1139 .filter_map(|b| b.param_name.as_deref())
1140 .collect();
1141
1142 content.push_str(&crate::template_env::render(
1143 "php_api_class_declaration.jinja",
1144 context! { class_name => &class_name },
1145 ));
1146 for func in &api.functions {
1147 let return_type = php_type_fq(&func.return_type, &namespace);
1148 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1149 let visible_params: Vec<_> = func
1151 .params
1152 .iter()
1153 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1154 .collect();
1155 let has_array_params = visible_params
1162 .iter()
1163 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1164 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1165 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1166 if has_array_params || has_array_return {
1167 content.push_str(" /**\n");
1168 for p in &visible_params {
1169 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1170 let nullable_prefix = if p.optional { "?" } else { "" };
1171 content.push_str(&crate::template_env::render(
1172 "php_phpdoc_static_param.jinja",
1173 context! {
1174 nullable_prefix => nullable_prefix,
1175 ptype => &ptype,
1176 param_name => &p.name,
1177 },
1178 ));
1179 }
1180 content.push_str(&crate::template_env::render(
1181 "php_phpdoc_static_return.jinja",
1182 context! { return_phpdoc => &return_phpdoc },
1183 ));
1184 content.push_str(" */\n");
1185 }
1186 let params: Vec<String> = visible_params
1187 .iter()
1188 .map(|p| {
1189 let ptype = php_type_fq(&p.ty, &namespace);
1190 if p.optional {
1191 format!("?{} ${} = null", ptype, p.name)
1192 } else {
1193 format!("{} ${}", ptype, p.name)
1194 }
1195 })
1196 .collect();
1197 let stub_method_name = if func.is_async {
1199 format!("{}_async", func.name).to_lower_camel_case()
1200 } else {
1201 func.name.to_lower_camel_case()
1202 };
1203 let is_void_stub = return_type == "void";
1204 let stub_body = if is_void_stub {
1205 "{ }".to_string()
1206 } else {
1207 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1208 };
1209 content.push_str(&crate::template_env::render(
1210 "php_static_method_stub.jinja",
1211 context! {
1212 method_name => &stub_method_name,
1213 params => ¶ms.join(", "),
1214 return_type => &return_type,
1215 stub_body => &stub_body,
1216 },
1217 ));
1218 }
1219 content.push_str("}\n\n");
1220 }
1221
1222 content.push_str(&crate::template_env::render(
1224 "php_namespace_block_end.jinja",
1225 minijinja::Value::default(),
1226 ));
1227
1228 let output_dir = config
1230 .php
1231 .as_ref()
1232 .and_then(|p| p.stubs.as_ref())
1233 .map(|s| s.output.to_string_lossy().to_string())
1234 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1235
1236 Ok(vec![GeneratedFile {
1237 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1238 content,
1239 generated_header: false,
1240 }])
1241 }
1242
1243 fn build_config(&self) -> Option<BuildConfig> {
1244 Some(BuildConfig {
1245 tool: "cargo",
1246 crate_suffix: "-php",
1247 build_dep: BuildDependency::None,
1248 post_build: vec![],
1249 })
1250 }
1251}
1252
1253fn php_phpdoc_type(ty: &TypeRef) -> String {
1256 match ty {
1257 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1258 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1259 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1260 _ => php_type(ty),
1261 }
1262}
1263
1264fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1266 match ty {
1267 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1268 TypeRef::Map(k, v) => format!(
1269 "array<{}, {}>",
1270 php_phpdoc_type_fq(k, namespace),
1271 php_phpdoc_type_fq(v, namespace)
1272 ),
1273 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1274 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1275 _ => php_type(ty),
1276 }
1277}
1278
1279fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1281 match ty {
1282 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1283 TypeRef::Optional(inner) => {
1284 let inner_type = php_type_fq(inner, namespace);
1285 if inner_type.starts_with('?') {
1286 inner_type
1287 } else {
1288 format!("?{inner_type}")
1289 }
1290 }
1291 _ => php_type(ty),
1292 }
1293}
1294
1295fn php_type(ty: &TypeRef) -> String {
1297 match ty {
1298 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1299 TypeRef::Primitive(p) => match p {
1300 PrimitiveType::Bool => "bool".to_string(),
1301 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1302 PrimitiveType::U8
1303 | PrimitiveType::U16
1304 | PrimitiveType::U32
1305 | PrimitiveType::U64
1306 | PrimitiveType::I8
1307 | PrimitiveType::I16
1308 | PrimitiveType::I32
1309 | PrimitiveType::I64
1310 | PrimitiveType::Usize
1311 | PrimitiveType::Isize => "int".to_string(),
1312 },
1313 TypeRef::Optional(inner) => {
1314 let inner_type = php_type(inner);
1317 if inner_type.starts_with('?') {
1318 inner_type
1319 } else {
1320 format!("?{inner_type}")
1321 }
1322 }
1323 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1324 TypeRef::Named(name) => name.clone(),
1325 TypeRef::Unit => "void".to_string(),
1326 TypeRef::Duration => "float".to_string(),
1327 }
1328}