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