1mod functions;
2mod helpers;
3pub mod 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_codegen::naming::to_php_name;
12use alef_codegen::shared::binding_fields;
13use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
14use alef_core::config::{Language, ResolvedCrateConfig, detect_serde_available, resolve_output_dir};
15use alef_core::hash::{self, CommentStyle};
16use alef_core::ir::ApiSurface;
17use alef_core::ir::{PrimitiveType, TypeRef};
18use heck::{ToLowerCamelCase, ToPascalCase};
19use minijinja::context;
20use std::collections::HashMap;
21use std::path::PathBuf;
22
23use crate::naming::php_autoload_namespace;
24use functions::{gen_async_function_as_static_method, gen_function_as_static_method};
25
26fn sanitize_php_enum_case(name: &str) -> String {
29 if name.eq_ignore_ascii_case("class") {
30 format!("{name}_")
31 } else {
32 name.to_string()
33 }
34}
35use helpers::{gen_enum_tainted_from_binding_to_core, gen_tokio_runtime, has_enum_named_field, references_named_type};
36use types::{
37 gen_enum_constants, gen_flat_data_enum, gen_flat_data_enum_from_impls, gen_flat_data_enum_methods, gen_php_struct,
38 is_tagged_data_enum, is_untagged_data_enum,
39};
40
41pub struct PhpBackend;
42
43impl PhpBackend {
44 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
45 RustBindingConfig {
46 struct_attrs: &["php_class"],
47 field_attrs: &[],
48 struct_derives: &["Clone"],
49 method_block_attr: Some("php_impl"),
50 constructor_attr: "",
51 static_attr: None,
52 function_attr: "#[php_function]",
53 enum_attrs: &[],
54 enum_derives: &[],
55 needs_signature: false,
56 signature_prefix: "",
57 signature_suffix: "",
58 core_import,
59 async_pattern: AsyncPattern::TokioBlockOn,
60 has_serde,
61 type_name_prefix: "",
62 option_duration_on_defaults: true,
63 opaque_type_names: &[],
64 skip_impl_constructor: false,
65 cast_uints_to_i32: false,
66 cast_large_ints_to_f64: false,
67 named_non_opaque_params_by_ref: false,
68 lossy_skip_types: &[],
69 serializable_opaque_type_names: &[],
70 never_skip_cfg_field_names: &[],
71 }
72 }
73}
74
75impl Backend for PhpBackend {
76 fn name(&self) -> &str {
77 "php"
78 }
79
80 fn language(&self) -> Language {
81 Language::Php
82 }
83
84 fn capabilities(&self) -> Capabilities {
85 Capabilities {
86 supports_async: false,
87 supports_classes: true,
88 supports_enums: true,
89 supports_option: true,
90 supports_result: true,
91 ..Capabilities::default()
92 }
93 }
94
95 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
96 let data_enum_names: AHashSet<String> = api
99 .enums
100 .iter()
101 .filter(|e| is_tagged_data_enum(e))
102 .map(|e| e.name.clone())
103 .collect();
104 let untagged_data_enum_names: AHashSet<String> = api
105 .enums
106 .iter()
107 .filter(|e| is_untagged_data_enum(e))
108 .map(|e| e.name.clone())
109 .collect();
110 let enum_names: AHashSet<String> = api
113 .enums
114 .iter()
115 .filter(|e| !is_tagged_data_enum(e) && !is_untagged_data_enum(e))
116 .map(|e| e.name.clone())
117 .collect();
118 let mapper = PhpMapper {
119 enum_names: enum_names.clone(),
120 data_enum_names: data_enum_names.clone(),
121 untagged_data_enum_names: untagged_data_enum_names.clone(),
122 };
123 let core_import = config.core_import_name();
124 let lang_rename_all = config.serde_rename_all_for_language(Language::Php);
125
126 let php_config = config.php.as_ref();
128 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
129 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
130
131 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
132 let has_serde = detect_serde_available(&output_dir);
133
134 let bridge_type_aliases_php: Vec<String> = config
140 .trait_bridges
141 .iter()
142 .filter_map(|b| b.type_alias.clone())
143 .collect();
144 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
145 let mut opaque_names_vec_php: Vec<String> = api
146 .types
147 .iter()
148 .filter(|t| t.is_opaque)
149 .map(|t| t.name.clone())
150 .collect();
151 opaque_names_vec_php.extend(bridge_type_aliases_php);
152
153 let mut cfg = Self::binding_config(&core_import, has_serde);
154 cfg.opaque_type_names = &opaque_names_vec_php;
155 let never_skip_cfg_field_names: Vec<String> = config
156 .trait_bridges
157 .iter()
158 .filter_map(|b| {
159 if b.bind_via == alef_core::config::BridgeBinding::OptionsField {
160 b.resolved_options_field().map(|s| s.to_string())
161 } else {
162 None
163 }
164 })
165 .collect();
166 cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;
167
168 let mut builder = RustFileBuilder::new().with_generated_header();
170 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
171 builder.add_inner_attribute("allow(unsafe_code)");
172 builder.add_inner_attribute("allow(non_snake_case)");
174 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, clippy::should_implement_trait, clippy::useless_conversion)");
175 builder.add_import("ext_php_rs::prelude::*");
176
177 if has_serde {
179 builder.add_import("serde_json");
180 }
181
182 for trait_path in generators::collect_trait_imports(api) {
184 builder.add_import(&trait_path);
185 }
186
187 let has_maps = api.types.iter().any(|t| {
189 t.fields
190 .iter()
191 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
192 }) || api
193 .functions
194 .iter()
195 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
196 if has_maps {
197 builder.add_import("std::collections::HashMap");
198 }
199
200 builder.add_item(
205 "#[derive(Debug, Clone, Default)]\n\
206 pub struct PhpBytes(pub Vec<u8>);\n\
207 \n\
208 impl<'a> ext_php_rs::convert::FromZval<'a> for PhpBytes {\n \
209 const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::String;\n \
210 fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {\n \
211 zval.zend_str().map(|zs| PhpBytes(zs.as_bytes().to_vec()))\n \
212 }\n\
213 }\n\
214 \n\
215 impl From<PhpBytes> for Vec<u8> {\n \
216 fn from(b: PhpBytes) -> Self { b.0 }\n\
217 }\n\
218 \n\
219 impl From<Vec<u8>> for PhpBytes {\n \
220 fn from(v: Vec<u8>) -> Self { PhpBytes(v) }\n\
221 }\n",
222 );
223
224 let custom_mods = config.custom_modules.for_language(Language::Php);
226 for module in custom_mods {
227 builder.add_item(&format!("pub mod {module};"));
228 }
229
230 let has_async =
232 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
233
234 if has_async {
235 builder.add_item(&gen_tokio_runtime());
236 }
237
238 let opaque_types: AHashSet<String> = api
240 .types
241 .iter()
242 .filter(|t| t.is_opaque)
243 .map(|t| t.name.clone())
244 .collect();
245 if !opaque_types.is_empty() {
246 builder.add_import("std::sync::Arc");
247 }
248
249 let mutex_types: AHashSet<String> = api
251 .types
252 .iter()
253 .filter(|t| t.is_opaque && alef_codegen::generators::type_needs_mutex(t))
254 .map(|t| t.name.clone())
255 .collect();
256 if !mutex_types.is_empty() {
257 builder.add_import("std::sync::Mutex");
258 }
259
260 let extension_name = config.php_extension_name();
263 let php_namespace = php_autoload_namespace(config);
264
265 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
267
268 let streaming_method_keys: AHashSet<String> = config
273 .adapters
274 .iter()
275 .filter(|a| matches!(a.pattern, alef_core::config::AdapterPattern::Streaming))
276 .filter_map(|a| a.owner_type.as_deref().map(|owner| format!("{owner}.{}", a.name)))
277 .collect();
278
279 for adapter in &config.adapters {
281 match adapter.pattern {
282 alef_core::config::AdapterPattern::Streaming => {
283 let key = alef_adapters::stream_struct_key(adapter);
284 if let Some(struct_code) = adapter_bodies.get(&key) {
285 builder.add_item(struct_code);
286 }
287 }
288 alef_core::config::AdapterPattern::CallbackBridge => {
289 let struct_key = format!("{}.__bridge_struct__", adapter.name);
290 let impl_key = format!("{}.__bridge_impl__", adapter.name);
291 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
292 builder.add_item(struct_code);
293 }
294 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
295 builder.add_item(impl_code);
296 }
297 }
298 _ => {}
299 }
300 }
301
302 for typ in api
303 .types
304 .iter()
305 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
306 {
307 if typ.is_opaque {
308 let ns_escaped = php_namespace.replace('\\', "\\\\");
312 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
313 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
314 let opaque_cfg = RustBindingConfig {
315 struct_attrs: &opaque_attr_arr,
316 ..cfg
317 };
318 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
319 builder.add_item(&types::gen_opaque_struct_methods_with_exclude(
320 typ,
321 &mapper,
322 &opaque_types,
323 &core_import,
324 &adapter_bodies,
325 &mutex_types,
326 &streaming_method_keys,
327 ));
328 if let Some(ctor) = config.client_constructors.get(&typ.name) {
330 let ctor_body = generators::gen_opaque_constructor(ctor, &typ.name, &core_import, "#[php_method]");
331 let ctor_impl = format!("#[php_impl]\nimpl {} {{\n{}}}", typ.name, ctor_body);
332 builder.add_item(&ctor_impl);
333 }
334 } else {
335 builder.add_item(&gen_php_struct(
338 typ,
339 &mapper,
340 &cfg,
341 Some(&php_namespace),
342 &enum_names,
343 &lang_rename_all,
344 ));
345 builder.add_item(&types::gen_struct_methods_with_exclude(
346 typ,
347 &mapper,
348 has_serde,
349 &core_import,
350 &opaque_types,
351 &enum_names,
352 &api.enums,
353 &exclude_functions,
354 &bridge_type_aliases_set,
355 &never_skip_cfg_field_names,
356 &mutex_types,
357 ));
358 }
359 }
360
361 for enum_def in &api.enums {
362 if is_tagged_data_enum(enum_def) {
363 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
365 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
366 } else {
367 builder.add_item(&gen_enum_constants(enum_def));
368 }
369 }
370
371 let included_functions: Vec<_> = api
376 .functions
377 .iter()
378 .filter(|f| !exclude_functions.contains(&f.name))
379 .collect();
380 if !included_functions.is_empty() {
381 let facade_class_name = extension_name.to_pascal_case();
382 let mut method_items: Vec<String> = Vec::new();
385 for func in included_functions {
386 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
387 if let Some((param_idx, bridge_cfg)) = bridge_param {
388 let bridge_handle_path = bridge_handle_path(api, bridge_cfg, &core_import);
389 method_items.push(crate::trait_bridge::gen_bridge_function(
390 func,
391 param_idx,
392 bridge_cfg,
393 &mapper,
394 &opaque_types,
395 &core_import,
396 &bridge_handle_path,
397 ));
398 } else if func.is_async {
399 method_items.push(gen_async_function_as_static_method(
400 func,
401 &mapper,
402 &opaque_types,
403 &core_import,
404 &config.trait_bridges,
405 &mutex_types,
406 ));
407 } else {
408 method_items.push(gen_function_as_static_method(
409 func,
410 &mapper,
411 &opaque_types,
412 &core_import,
413 &config.trait_bridges,
414 has_serde,
415 &mutex_types,
416 ));
417 }
418 }
419
420 for bridge_cfg in &config.trait_bridges {
422 if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
423 method_items.push(format!(
424 "pub fn {}(backend: &mut ext_php_rs::types::ZendObject) -> ext_php_rs::prelude::PhpResult<()> {{\n \
425 crate::{}(backend)\n}}",
426 register_fn,
427 register_fn
428 ));
429 }
430 if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
431 method_items.push(format!(
432 "pub fn {}(name: String) -> ext_php_rs::prelude::PhpResult<()> {{\n \
433 crate::{}(name)\n}}",
434 unregister_fn, unregister_fn
435 ));
436 }
437 if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
438 method_items.push(format!(
439 "pub fn {}() -> ext_php_rs::prelude::PhpResult<()> {{\n \
440 crate::{}()\n}}",
441 clear_fn, clear_fn
442 ));
443 }
444 }
445
446 let methods_joined = method_items
447 .iter()
448 .map(|m| {
449 m.lines()
451 .map(|l| {
452 if l.is_empty() {
453 String::new()
454 } else {
455 format!(" {l}")
456 }
457 })
458 .collect::<Vec<_>>()
459 .join("\n")
460 })
461 .collect::<Vec<_>>()
462 .join("\n\n");
463 let php_api_class_name = format!("{facade_class_name}Api");
466 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
468 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
469 let facade_struct = format!(
470 "#[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}}"
471 );
472 builder.add_item(&facade_struct);
473
474 for bridge_cfg in &config.trait_bridges {
476 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
477 let bridge = crate::trait_bridge::gen_trait_bridge(
478 trait_type,
479 bridge_cfg,
480 &core_import,
481 &config.error_type_name(),
482 &config.error_constructor_expr(),
483 api,
484 );
485 for imp in &bridge.imports {
486 builder.add_import(imp);
487 }
488 builder.add_item(&bridge.code);
489 }
490 }
491 }
492
493 let convertible = alef_codegen::conversions::convertible_types(api);
494 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
495 let input_types = alef_codegen::conversions::input_type_names(api);
496 let enum_names_ref = &mapper.enum_names;
501 let bridge_skip_types: Vec<String> = config
502 .trait_bridges
503 .iter()
504 .filter(|b| !matches!(b.bind_via, alef_core::config::BridgeBinding::OptionsField))
505 .filter_map(|b| b.type_alias.clone())
506 .collect();
507 let trait_bridge_arc_wrapper_field_names: Vec<String> = config
512 .trait_bridges
513 .iter()
514 .filter(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField)
515 .filter_map(|b| b.resolved_options_field().map(String::from))
516 .collect();
517 let mut conv_opaque_types: AHashSet<String> = opaque_types.clone();
522 for bridge in &config.trait_bridges {
523 if let Some(alias) = &bridge.type_alias {
524 conv_opaque_types.insert(alias.clone());
525 }
526 }
527 let php_conv_config = ConversionConfig {
528 cast_large_ints_to_i64: true,
529 enum_string_names: Some(enum_names_ref),
530 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
531 json_as_value: true,
535 include_cfg_metadata: false,
536 option_duration_on_defaults: true,
537 from_binding_skip_types: &bridge_skip_types,
538 never_skip_cfg_field_names: &never_skip_cfg_field_names,
539 opaque_types: Some(&conv_opaque_types),
540 trait_bridge_arc_wrapper_field_names: &trait_bridge_arc_wrapper_field_names,
541 ..Default::default()
542 };
543 let mut enum_tainted: AHashSet<String> = AHashSet::new();
545 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
546 if has_enum_named_field(typ, enum_names_ref) {
547 enum_tainted.insert(typ.name.clone());
548 }
549 }
550 let mut changed = true;
552 while changed {
553 changed = false;
554 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
555 if !enum_tainted.contains(&typ.name)
556 && binding_fields(&typ.fields).any(|f| references_named_type(&f.ty, &enum_tainted))
557 {
558 enum_tainted.insert(typ.name.clone());
559 changed = true;
560 }
561 }
562 }
563 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
564 if input_types.contains(&typ.name)
566 && !enum_tainted.contains(&typ.name)
567 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
568 {
569 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
570 typ,
571 &core_import,
572 &php_conv_config,
573 ));
574 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
575 builder.add_item(&gen_enum_tainted_from_binding_to_core(
582 typ,
583 &core_import,
584 enum_names_ref,
585 &enum_tainted,
586 &php_conv_config,
587 &api.enums,
588 &bridge_type_aliases_set,
589 ));
590 }
591 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
593 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
594 typ,
595 &core_import,
596 &opaque_types,
597 &php_conv_config,
598 ));
599 }
600 }
601
602 let mut emitted_binding_to_core: AHashSet<String> = api
612 .types
613 .iter()
614 .filter(|typ| !typ.is_trait && input_types.contains(&typ.name))
615 .filter(|typ| {
616 (enum_tainted.contains(&typ.name))
617 || alef_codegen::conversions::can_generate_conversion(typ, &convertible)
618 })
619 .map(|typ| typ.name.clone())
620 .collect();
621 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
622 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
623 for variant in &enum_def.variants {
626 for field in &variant.fields {
627 if let TypeRef::Named(type_name) = &field.ty {
628 if let Some(typ) = api.types.iter().find(|t| &t.name == type_name) {
629 if emitted_binding_to_core.contains(&typ.name) {
630 continue;
631 }
632 if enum_tainted.contains(&typ.name) {
633 builder.add_item(&gen_enum_tainted_from_binding_to_core(
634 typ,
635 &core_import,
636 enum_names_ref,
637 &enum_tainted,
638 &php_conv_config,
639 &api.enums,
640 &bridge_type_aliases_set,
641 ));
642 emitted_binding_to_core.insert(typ.name.clone());
643 } else if alef_codegen::conversions::can_generate_conversion(typ, &convertible) {
644 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
645 typ,
646 &core_import,
647 &php_conv_config,
648 ));
649 emitted_binding_to_core.insert(typ.name.clone());
650 }
651 }
652 }
653 }
654 }
655 }
656
657 for typ in api.types.iter().filter(|t| !t.is_trait) {
661 if !emitted_binding_to_core.contains(&typ.name) {
662 if enum_tainted.contains(&typ.name) {
663 builder.add_item(&gen_enum_tainted_from_binding_to_core(
664 typ,
665 &core_import,
666 enum_names_ref,
667 &enum_tainted,
668 &php_conv_config,
669 &api.enums,
670 &bridge_type_aliases_set,
671 ));
672 emitted_binding_to_core.insert(typ.name.clone());
673 } else if alef_codegen::conversions::can_generate_conversion(typ, &convertible) {
674 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
675 typ,
676 &core_import,
677 &php_conv_config,
678 ));
679 emitted_binding_to_core.insert(typ.name.clone());
680 }
681 }
682 }
683
684 for error in &api.errors {
686 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
687 let methods_impl = alef_codegen::error_gen::gen_php_error_methods_impl(error, &core_import);
689 if !methods_impl.is_empty() {
690 builder.add_item(&methods_impl);
691 }
692 }
693
694 if has_serde {
698 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
699 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
700 pub fn max_compression_ratio() -> i64 { 100 }\n\
701 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
702 pub fn max_nesting_depth() -> i64 { 1024 }\n\
703 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
704 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
705 pub fn max_iterations() -> i64 { 10_000_000 }\n\
706 pub fn max_xml_depth() -> i64 { 1024 }\n\
707 pub fn max_table_cells() -> i64 { 100_000 }\n\
708 }";
709 builder.add_item(serde_module);
710 }
711
712 let php_config = config.php.as_ref();
718 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
719
720 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
724 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
725 }
726
727 let mut class_registrations = String::new();
730 for typ in api
731 .types
732 .iter()
733 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
734 {
735 class_registrations.push_str(&crate::template_env::render(
736 "php_class_registration.jinja",
737 context! { class_name => &typ.name },
738 ));
739 }
740 if !api.functions.is_empty() {
742 let facade_class_name = extension_name.to_pascal_case();
743 class_registrations.push_str(&crate::template_env::render(
744 "php_class_registration.jinja",
745 context! { class_name => &format!("{facade_class_name}Api") },
746 ));
747 }
748 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
751 class_registrations.push_str(&crate::template_env::render(
752 "php_class_registration.jinja",
753 context! { class_name => &enum_def.name },
754 ));
755 }
756 for error in api.errors.iter().filter(|e| !e.methods.is_empty()) {
758 let info_class = format!("{}Info", error.name);
759 class_registrations.push_str(&crate::template_env::render(
760 "php_class_registration.jinja",
761 context! { class_name => &info_class },
762 ));
763 }
764 builder.add_item(&format!(
765 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
766 ));
767
768 let mut content = builder.build();
769
770 for bridge in &config.trait_bridges {
775 if let Some(field_name) = bridge.resolved_options_field() {
776 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
777 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
778 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
779 let builder_type = format!("{}Builder", options_type);
780 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
781 let bridge_handle_path = bridge_handle_path(api, bridge, &core_import);
782
783 let old_method = format!(
789 " 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 }}"
790 );
791 let new_method = format!(
792 " 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: {bridge_handle_path} = std::sync::Arc::new(std::sync::Mutex::new(bridge));\n Self {{ inner: Arc::new((*self.inner).clone().{field_name}(Some(handle))) }}\n }}"
793 );
794
795 content = content.replace(&old_method, &new_method);
796 }
797 }
798
799 let php_stubs_dir = config
802 .php
803 .as_ref()
804 .and_then(|p| p.stubs.as_ref())
805 .map(|s| s.output.to_string_lossy().to_string())
806 .unwrap_or_else(|| "packages/php/src/".to_string());
807
808 let php_namespace = php_autoload_namespace(config);
809
810 let mut generated_files = vec![GeneratedFile {
811 path: PathBuf::from(&output_dir).join("lib.rs"),
812 content,
813 generated_header: false,
814 }];
815
816 for bridge_cfg in &config.trait_bridges {
818 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
819 let is_visitor_bridge = bridge_cfg.type_alias.is_some()
821 && bridge_cfg.register_fn.is_none()
822 && bridge_cfg.super_trait.is_none()
823 && trait_type.methods.iter().all(|m| m.has_default_impl);
824
825 if is_visitor_bridge {
826 let interface_content = crate::trait_bridge::gen_visitor_interface(
827 trait_type,
828 bridge_cfg,
829 &php_namespace,
830 &HashMap::new(), );
832 let interface_filename = format!("{}Interface.php", bridge_cfg.trait_name);
833 generated_files.push(GeneratedFile {
834 path: PathBuf::from(&php_stubs_dir).join(&interface_filename),
835 content: interface_content,
836 generated_header: false,
837 });
838 }
839 }
840 }
841
842 Ok(generated_files)
843 }
844
845 fn generate_public_api(
846 &self,
847 api: &ApiSurface,
848 config: &ResolvedCrateConfig,
849 ) -> anyhow::Result<Vec<GeneratedFile>> {
850 let extension_name = config.php_extension_name();
851 let class_name = extension_name.to_pascal_case();
852
853 let mut content = String::new();
855 content.push_str(&crate::template_env::render(
856 "php_file_header.jinja",
857 minijinja::Value::default(),
858 ));
859 content.push_str(&hash::header(CommentStyle::DoubleSlash));
860 content.push_str(&crate::template_env::render(
861 "php_declare_strict_types.jinja",
862 minijinja::Value::default(),
863 ));
864 content.push('\n');
866
867 let namespace = php_autoload_namespace(config);
869
870 content.push_str(&crate::template_env::render(
871 "php_namespace.jinja",
872 context! { namespace => &namespace },
873 ));
874 content.push('\n');
876 content.push_str(&crate::template_env::render(
877 "php_facade_class_declaration.jinja",
878 context! { class_name => &class_name },
879 ));
880
881 let bridge_param_names_pub: ahash::AHashSet<&str> = config
883 .trait_bridges
884 .iter()
885 .filter_map(|b| b.param_name.as_deref())
886 .collect();
887
888 let no_arg_constructor_types: AHashSet<String> = api
893 .types
894 .iter()
895 .filter(|t| t.fields.iter().all(|f| f.optional))
896 .map(|t| t.name.clone())
897 .collect();
898
899 for func in &api.functions {
901 let method_name = func.name.to_lower_camel_case();
906 let return_php_type = php_type(&func.return_type);
907
908 let visible_params: Vec<_> = func
910 .params
911 .iter()
912 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
913 .collect();
914
915 content.push_str(&crate::template_env::render(
917 "php_phpdoc_block_start.jinja",
918 minijinja::Value::default(),
919 ));
920 if func.doc.is_empty() {
921 content.push_str(&crate::template_env::render(
922 "php_phpdoc_text_line.jinja",
923 context! { text => &format!("{}.", method_name) },
924 ));
925 } else {
926 content.push_str(&crate::template_env::render(
927 "php_phpdoc_lines.jinja",
928 context! {
929 doc_lines => func.doc.lines().collect::<Vec<_>>(),
930 indent => " ",
931 },
932 ));
933 }
934 content.push_str(&crate::template_env::render(
935 "php_phpdoc_empty_line.jinja",
936 minijinja::Value::default(),
937 ));
938 for p in &visible_params {
939 let ptype = php_phpdoc_type(&p.ty);
940 let nullable_prefix = if p.optional { "?" } else { "" };
941 content.push_str(&crate::template_env::render(
942 "php_phpdoc_param_line.jinja",
943 context! {
944 nullable_prefix => nullable_prefix,
945 param_type => &ptype,
946 param_name => &p.name,
947 },
948 ));
949 }
950 let return_phpdoc = php_phpdoc_type(&func.return_type);
951 content.push_str(&crate::template_env::render(
952 "php_phpdoc_return_line.jinja",
953 context! { return_type => &return_phpdoc },
954 ));
955 if func.error_type.is_some() {
956 content.push_str(&crate::template_env::render(
957 "php_phpdoc_throws_line.jinja",
958 context! {
959 namespace => namespace.as_str(),
960 class_name => &class_name,
961 },
962 ));
963 }
964 content.push_str(&crate::template_env::render(
965 "php_phpdoc_block_end.jinja",
966 minijinja::Value::default(),
967 ));
968
969 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
980 if let TypeRef::Named(name) = &p.ty {
981 (name.ends_with("Config") || name.as_str() == "config")
982 && no_arg_constructor_types.contains(name.as_str())
983 } else {
984 false
985 }
986 };
987
988 let mut first_optional_idx = None;
989 for (idx, p) in visible_params.iter().enumerate() {
990 if p.optional || is_optional_config_param(p) {
991 first_optional_idx = Some(idx);
992 break;
993 }
994 }
995
996 content.push_str(&crate::template_env::render(
997 "php_method_signature_start.jinja",
998 context! { method_name => &method_name },
999 ));
1000
1001 let params: Vec<String> = visible_params
1002 .iter()
1003 .enumerate()
1004 .map(|(idx, p)| {
1005 let ptype = php_type(&p.ty);
1006 let should_be_optional = p.optional
1011 || is_optional_config_param(p)
1012 || first_optional_idx.is_some_and(|first| idx >= first);
1013 if should_be_optional {
1014 format!("?{} ${} = null", ptype, p.name)
1015 } else {
1016 format!("{} ${}", ptype, p.name)
1017 }
1018 })
1019 .collect();
1020 content.push_str(¶ms.join(", "));
1021 content.push_str(&crate::template_env::render(
1022 "php_method_signature_end.jinja",
1023 context! { return_type => &return_php_type },
1024 ));
1025 let ext_method_name = func.name.to_lower_camel_case();
1030 let is_void = matches!(&func.return_type, TypeRef::Unit);
1031 let call_params = visible_params
1039 .iter()
1040 .enumerate()
1041 .map(|(idx, p)| {
1042 let should_be_optional = p.optional
1043 || is_optional_config_param(p)
1044 || first_optional_idx.is_some_and(|first| idx >= first);
1045 if should_be_optional && is_optional_config_param(p) {
1046 if let TypeRef::Named(type_name) = &p.ty {
1047 return format!("${} ?? new {}()", p.name, type_name);
1048 }
1049 }
1050 format!("${}", p.name)
1051 })
1052 .collect::<Vec<_>>()
1053 .join(", ");
1054 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
1055 if is_void {
1056 content.push_str(&crate::template_env::render(
1057 "php_method_call_statement.jinja",
1058 context! { call_expr => &call_expr },
1059 ));
1060 } else {
1061 content.push_str(&crate::template_env::render(
1062 "php_method_call_return.jinja",
1063 context! { call_expr => &call_expr },
1064 ));
1065 }
1066 content.push_str(&crate::template_env::render(
1067 "php_method_end.jinja",
1068 minijinja::Value::default(),
1069 ));
1070 }
1071
1072 for bridge_cfg in &config.trait_bridges {
1074 if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
1075 let method_name = register_fn.to_lower_camel_case();
1076 content.push_str(&crate::template_env::render(
1077 "php_phpdoc_block_start.jinja",
1078 minijinja::Value::default(),
1079 ));
1080 content.push_str(&crate::template_env::render(
1081 "php_phpdoc_text_line.jinja",
1082 context! { text => &format!("{}.", method_name) },
1083 ));
1084 content.push_str(&crate::template_env::render(
1085 "php_phpdoc_empty_line.jinja",
1086 minijinja::Value::default(),
1087 ));
1088 let interface_name = &bridge_cfg.trait_name;
1089 content.push_str(&crate::template_env::render(
1090 "php_phpdoc_param_line.jinja",
1091 context! {
1092 nullable_prefix => "",
1093 param_type => interface_name,
1094 param_name => "backend",
1095 },
1096 ));
1097 content.push_str(&crate::template_env::render(
1098 "php_phpdoc_return_line.jinja",
1099 context! { return_type => "void" },
1100 ));
1101 content.push_str(&crate::template_env::render(
1102 "php_phpdoc_block_end.jinja",
1103 minijinja::Value::default(),
1104 ));
1105 content.push_str(&crate::template_env::render(
1106 "php_method_signature_start.jinja",
1107 context! { method_name => &method_name },
1108 ));
1109 content.push_str(&format!("{} $backend = null) : void", interface_name));
1110 let call_expr = format!("\\{namespace}\\{class_name}Api::{register_fn}($backend)");
1111 content.push_str(&crate::template_env::render(
1112 "php_method_call_statement.jinja",
1113 context! { call_expr => &call_expr },
1114 ));
1115 content.push_str(&crate::template_env::render(
1116 "php_method_end.jinja",
1117 minijinja::Value::default(),
1118 ));
1119 }
1120 if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
1121 let method_name = unregister_fn.to_lower_camel_case();
1122 content.push_str(&crate::template_env::render(
1123 "php_phpdoc_block_start.jinja",
1124 minijinja::Value::default(),
1125 ));
1126 content.push_str(&crate::template_env::render(
1127 "php_phpdoc_text_line.jinja",
1128 context! { text => &format!("{}.", method_name) },
1129 ));
1130 content.push_str(&crate::template_env::render(
1131 "php_phpdoc_empty_line.jinja",
1132 minijinja::Value::default(),
1133 ));
1134 content.push_str(&crate::template_env::render(
1135 "php_phpdoc_param_line.jinja",
1136 context! {
1137 nullable_prefix => "",
1138 param_type => "string",
1139 param_name => "name",
1140 },
1141 ));
1142 content.push_str(&crate::template_env::render(
1143 "php_phpdoc_return_line.jinja",
1144 context! { return_type => "void" },
1145 ));
1146 content.push_str(&crate::template_env::render(
1147 "php_phpdoc_block_end.jinja",
1148 minijinja::Value::default(),
1149 ));
1150 content.push_str(&crate::template_env::render(
1151 "php_method_signature_start.jinja",
1152 context! { method_name => &method_name },
1153 ));
1154 content.push_str("string $name) : void");
1155 let call_expr = format!("\\{namespace}\\{class_name}Api::{unregister_fn}($name)");
1156 content.push_str(&crate::template_env::render(
1157 "php_method_call_statement.jinja",
1158 context! { call_expr => &call_expr },
1159 ));
1160 content.push_str(&crate::template_env::render(
1161 "php_method_end.jinja",
1162 minijinja::Value::default(),
1163 ));
1164 }
1165 if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
1166 let method_name = clear_fn.to_lower_camel_case();
1167 content.push_str(&crate::template_env::render(
1168 "php_phpdoc_block_start.jinja",
1169 minijinja::Value::default(),
1170 ));
1171 content.push_str(&crate::template_env::render(
1172 "php_phpdoc_text_line.jinja",
1173 context! { text => &format!("{}.", method_name) },
1174 ));
1175 content.push_str(&crate::template_env::render(
1176 "php_phpdoc_empty_line.jinja",
1177 minijinja::Value::default(),
1178 ));
1179 content.push_str(&crate::template_env::render(
1180 "php_phpdoc_return_line.jinja",
1181 context! { return_type => "void" },
1182 ));
1183 content.push_str(&crate::template_env::render(
1184 "php_phpdoc_block_end.jinja",
1185 minijinja::Value::default(),
1186 ));
1187 content.push_str(&crate::template_env::render(
1188 "php_method_signature_start.jinja",
1189 context! { method_name => &method_name },
1190 ));
1191 content.push_str(") : void");
1192 let call_expr = format!("\\{namespace}\\{class_name}Api::{clear_fn}()");
1193 content.push_str(&crate::template_env::render(
1194 "php_method_call_statement.jinja",
1195 context! { call_expr => &call_expr },
1196 ));
1197 content.push_str(&crate::template_env::render(
1198 "php_method_end.jinja",
1199 minijinja::Value::default(),
1200 ));
1201 }
1202 }
1203
1204 content.push_str(&crate::template_env::render(
1205 "php_class_end.jinja",
1206 minijinja::Value::default(),
1207 ));
1208
1209 let output_dir = config
1213 .php
1214 .as_ref()
1215 .and_then(|p| p.stubs.as_ref())
1216 .map(|s| s.output.to_string_lossy().to_string())
1217 .unwrap_or_else(|| "packages/php/src/".to_string());
1218
1219 let mut files: Vec<GeneratedFile> = Vec::new();
1220 files.push(GeneratedFile {
1221 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
1222 content,
1223 generated_header: false,
1224 });
1225
1226 for typ in api.types.iter().filter(|t| t.is_opaque && !t.is_trait) {
1232 let streaming_adapters: Vec<&alef_core::config::AdapterConfig> = config
1233 .adapters
1234 .iter()
1235 .filter(|a| {
1236 matches!(a.pattern, alef_core::config::AdapterPattern::Streaming)
1237 && a.owner_type.as_deref() == Some(&typ.name)
1238 && !a.skip_languages.iter().any(|l| l == "php")
1239 })
1240 .collect();
1241 let streaming_method_names: AHashSet<String> = streaming_adapters.iter().map(|a| a.name.clone()).collect();
1242 let opaque_file = gen_php_opaque_class_file(typ, &namespace, &streaming_adapters, &streaming_method_names);
1243 files.push(GeneratedFile {
1244 path: PathBuf::from(&output_dir).join(format!("{}.php", typ.name)),
1245 content: opaque_file,
1246 generated_header: false,
1247 });
1248 }
1249
1250 Ok(files)
1251 }
1252
1253 fn generate_type_stubs(
1254 &self,
1255 api: &ApiSurface,
1256 config: &ResolvedCrateConfig,
1257 ) -> anyhow::Result<Vec<GeneratedFile>> {
1258 let extension_name = config.php_extension_name();
1259 let class_name = extension_name.to_pascal_case();
1260
1261 let namespace = php_autoload_namespace(config);
1263
1264 let mut content = String::new();
1269 content.push_str(&crate::template_env::render(
1270 "php_file_header.jinja",
1271 minijinja::Value::default(),
1272 ));
1273 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1274 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
1275 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
1276 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
1277 content.push_str(&crate::template_env::render(
1278 "php_declare_strict_types.jinja",
1279 minijinja::Value::default(),
1280 ));
1281 content.push('\n');
1283 content.push_str(&crate::template_env::render(
1285 "php_namespace_block_begin.jinja",
1286 context! { namespace => &namespace },
1287 ));
1288
1289 content.push_str(&crate::template_env::render(
1291 "php_exception_class_declaration.jinja",
1292 context! { class_name => &class_name },
1293 ));
1294 content.push_str(
1295 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1296 );
1297 let has_status_code = api
1300 .errors
1301 .iter()
1302 .any(|e| e.methods.iter().any(|m| m.name == "status_code"));
1303 let has_is_transient = api
1304 .errors
1305 .iter()
1306 .any(|e| e.methods.iter().any(|m| m.name == "is_transient"));
1307 let has_error_type = api
1308 .errors
1309 .iter()
1310 .any(|e| e.methods.iter().any(|m| m.name == "error_type"));
1311 if has_status_code {
1312 content.push_str(
1313 " /** HTTP status code for this error (0 means no associated status). */\n \
1314 public function statusCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1315 );
1316 }
1317 if has_is_transient {
1318 content.push_str(
1319 " /** Returns true if the error is transient and a retry may succeed. */\n \
1320 public function isTransient(): bool { throw new \\RuntimeException('Not implemented.'); }\n",
1321 );
1322 }
1323 if has_error_type {
1324 content.push_str(
1325 " /** Machine-readable error category string for matching and logging. */\n \
1326 public function errorType(): string { throw new \\RuntimeException('Not implemented.'); }\n",
1327 );
1328 }
1329 content.push_str("}\n\n");
1330
1331 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
1338 if typ.is_opaque || typ.fields.is_empty() {
1339 continue;
1340 }
1341 if !typ.doc.is_empty() {
1342 content.push_str("/**\n");
1343 content.push_str(&crate::template_env::render(
1344 "php_phpdoc_lines.jinja",
1345 context! {
1346 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
1347 indent => "",
1348 },
1349 ));
1350 content.push_str(" */\n");
1351 }
1352 content.push_str(&crate::template_env::render(
1353 "php_record_class_stub_declaration.jinja",
1354 context! { class_name => &typ.name },
1355 ));
1356
1357 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = binding_fields(&typ.fields).collect();
1360 sorted_fields.sort_by_key(|f| f.optional);
1361
1362 let params: Vec<String> = sorted_fields
1367 .iter()
1368 .map(|f| {
1369 let ptype = php_type(&f.ty);
1370 let nullable = if f.optional && !ptype.starts_with('?') {
1371 format!("?{ptype}")
1372 } else {
1373 ptype
1374 };
1375 let default = if f.optional { " = null" } else { "" };
1376 let php_name = to_php_name(&f.name);
1377 let phpdoc_type = php_phpdoc_type(&f.ty);
1378 let var_type = if f.optional && !phpdoc_type.starts_with('?') {
1379 format!("?{phpdoc_type}")
1380 } else {
1381 phpdoc_type
1382 };
1383 let phpdoc = php_property_phpdoc(&var_type, &f.doc, " ");
1384 format!("{phpdoc} public readonly {nullable} ${php_name}{default}",)
1385 })
1386 .collect();
1387 content.push_str(&crate::template_env::render(
1388 "php_constructor_method.jinja",
1389 context! { params => ¶ms.join(",\n") },
1390 ));
1391
1392 let non_excluded_methods: Vec<&alef_core::ir::MethodDef> = typ
1397 .methods
1398 .iter()
1399 .filter(|m| !m.binding_excluded && !m.sanitized)
1400 .collect();
1401 for method in non_excluded_methods {
1402 let method_name = method.name.to_lower_camel_case();
1403 let is_static = method.receiver.is_none();
1404 let return_type = php_type(&method.return_type);
1405 let first_optional_idx = method.params.iter().position(|p| p.optional);
1406 let params: Vec<String> = method
1407 .params
1408 .iter()
1409 .enumerate()
1410 .map(|(idx, p)| {
1411 let ptype = php_type(&p.ty);
1412 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1413 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1414 format!("{nullable}{ptype} ${} = null", p.name)
1415 } else {
1416 format!("{} ${}", ptype, p.name)
1417 }
1418 })
1419 .collect();
1420 let static_kw = if is_static { "static " } else { "" };
1421 let is_void = matches!(&method.return_type, TypeRef::Unit);
1422 let stub_body = if is_void {
1423 "{ }".to_string()
1424 } else {
1425 "{ throw new \\RuntimeException('Not implemented — provided by the native extension.'); }"
1426 .to_string()
1427 };
1428 content.push_str(&format!(
1429 " public {static_kw}function {method_name}({}): {return_type}\n {stub_body}\n",
1430 params.join(", ")
1431 ));
1432 }
1433
1434 content.push_str("}\n\n");
1435 }
1436
1437 for enum_def in &api.enums {
1440 if is_tagged_data_enum(enum_def) {
1441 if !enum_def.doc.is_empty() {
1443 content.push_str("/**\n");
1444 content.push_str(&crate::template_env::render(
1445 "php_phpdoc_lines.jinja",
1446 context! {
1447 doc_lines => enum_def.doc.lines().collect::<Vec<_>>(),
1448 indent => "",
1449 },
1450 ));
1451 content.push_str(" */\n");
1452 }
1453 content.push_str(&crate::template_env::render(
1454 "php_record_class_stub_declaration.jinja",
1455 context! { class_name => &enum_def.name },
1456 ));
1457 content.push_str("}\n\n");
1458 } else {
1459 content.push_str(&crate::template_env::render(
1461 "php_tagged_enum_declaration.jinja",
1462 context! { enum_name => &enum_def.name },
1463 ));
1464 for variant in &enum_def.variants {
1465 let case_name = sanitize_php_enum_case(&variant.name);
1466 content.push_str(&crate::template_env::render(
1467 "php_enum_variant_stub.jinja",
1468 context! {
1469 variant_name => case_name,
1470 value => &variant.name,
1471 },
1472 ));
1473 }
1474 content.push_str("}\n\n");
1475 }
1476 }
1477
1478 if !api.functions.is_empty() {
1483 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1485 .trait_bridges
1486 .iter()
1487 .filter_map(|b| b.param_name.as_deref())
1488 .collect();
1489
1490 content.push_str(&crate::template_env::render(
1491 "php_api_class_declaration.jinja",
1492 context! { class_name => &class_name },
1493 ));
1494 for func in &api.functions {
1495 let return_type = php_type_fq(&func.return_type, &namespace);
1496 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1497 let visible_params: Vec<_> = func
1499 .params
1500 .iter()
1501 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1502 .collect();
1503 let has_array_params = visible_params
1510 .iter()
1511 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1512 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1513 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1514 let first_optional_idx = visible_params.iter().position(|p| p.optional);
1515 if has_array_params || has_array_return {
1516 content.push_str(" /**\n");
1517 for (idx, p) in visible_params.iter().enumerate() {
1518 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1519 let nullable_prefix = if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1520 "?"
1521 } else {
1522 ""
1523 };
1524 content.push_str(&crate::template_env::render(
1525 "php_phpdoc_static_param.jinja",
1526 context! {
1527 nullable_prefix => nullable_prefix,
1528 ptype => &ptype,
1529 param_name => &p.name,
1530 },
1531 ));
1532 }
1533 content.push_str(&crate::template_env::render(
1534 "php_phpdoc_static_return.jinja",
1535 context! { return_phpdoc => &return_phpdoc },
1536 ));
1537 content.push_str(" */\n");
1538 }
1539 let params: Vec<String> = visible_params
1540 .iter()
1541 .enumerate()
1542 .map(|(idx, p)| {
1543 let ptype = php_type_fq(&p.ty, &namespace);
1544 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1545 let nullable_ptype = if ptype.starts_with('?') {
1546 ptype
1547 } else {
1548 format!("?{ptype}")
1549 };
1550 format!("{} ${} = null", nullable_ptype, p.name)
1551 } else {
1552 format!("{} ${}", ptype, p.name)
1553 }
1554 })
1555 .collect();
1556 let stub_method_name = func.name.to_lower_camel_case();
1560 let is_void_stub = return_type == "void";
1561 let stub_body = if is_void_stub {
1562 "{ }".to_string()
1563 } else {
1564 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1565 };
1566 content.push_str(&crate::template_env::render(
1567 "php_static_method_stub.jinja",
1568 context! {
1569 method_name => &stub_method_name,
1570 params => ¶ms.join(", "),
1571 return_type => &return_type,
1572 stub_body => &stub_body,
1573 },
1574 ));
1575 }
1576 content.push_str("}\n\n");
1577 }
1578
1579 content.push_str(&crate::template_env::render(
1581 "php_namespace_block_end.jinja",
1582 minijinja::Value::default(),
1583 ));
1584
1585 let output_dir = config
1587 .php
1588 .as_ref()
1589 .and_then(|p| p.stubs.as_ref())
1590 .map(|s| s.output.to_string_lossy().to_string())
1591 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1592
1593 Ok(vec![GeneratedFile {
1594 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1595 content,
1596 generated_header: false,
1597 }])
1598 }
1599
1600 fn build_config(&self) -> Option<BuildConfig> {
1601 Some(BuildConfig {
1602 tool: "cargo",
1603 crate_suffix: "-php",
1604 build_dep: BuildDependency::None,
1605 post_build: vec![],
1606 })
1607 }
1608}
1609
1610fn bridge_handle_path(api: &ApiSurface, bridge: &alef_core::config::TraitBridgeConfig, core_import: &str) -> String {
1611 let alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
1612 api.types
1613 .iter()
1614 .find(|t| t.name == alias && !t.rust_path.is_empty())
1615 .map(|t| t.rust_path.replace('-', "_"))
1616 .or_else(|| api.excluded_type_paths.get(alias).map(|path| path.replace('-', "_")))
1617 .unwrap_or_else(|| format!("{core_import}::visitor::{alias}"))
1618}
1619
1620fn php_phpdoc_type(ty: &TypeRef) -> String {
1623 match ty {
1624 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1625 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1626 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1627 _ => php_type(ty),
1628 }
1629}
1630
1631fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1633 match ty {
1634 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1635 TypeRef::Map(k, v) => format!(
1636 "array<{}, {}>",
1637 php_phpdoc_type_fq(k, namespace),
1638 php_phpdoc_type_fq(v, namespace)
1639 ),
1640 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1641 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1642 _ => php_type(ty),
1643 }
1644}
1645
1646fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1648 match ty {
1649 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1650 TypeRef::Optional(inner) => {
1651 let inner_type = php_type_fq(inner, namespace);
1652 if inner_type.starts_with('?') {
1653 inner_type
1654 } else {
1655 format!("?{inner_type}")
1656 }
1657 }
1658 _ => php_type(ty),
1659 }
1660}
1661
1662fn gen_php_opaque_class_file(
1669 typ: &alef_core::ir::TypeDef,
1670 namespace: &str,
1671 streaming_adapters: &[&alef_core::config::AdapterConfig],
1672 streaming_method_names: &AHashSet<String>,
1673) -> String {
1674 let mut content = String::new();
1675 content.push_str(&crate::template_env::render(
1676 "php_file_header.jinja",
1677 minijinja::Value::default(),
1678 ));
1679 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1680 content.push_str(&crate::template_env::render(
1681 "php_declare_strict_types.jinja",
1682 minijinja::Value::default(),
1683 ));
1684 content.push('\n');
1686 content.push_str(&crate::template_env::render(
1687 "php_namespace.jinja",
1688 context! { namespace => namespace },
1689 ));
1690 content.push('\n');
1692
1693 if !typ.doc.is_empty() {
1695 content.push_str("/**\n");
1696 content.push_str(&crate::template_env::render(
1697 "php_phpdoc_lines.jinja",
1698 context! {
1699 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
1700 indent => "",
1701 },
1702 ));
1703 content.push_str(" */\n");
1704 }
1705
1706 content.push_str(&format!("final class {}\n{{\n", typ.name));
1707
1708 let mut method_order: Vec<&alef_core::ir::MethodDef> = Vec::new();
1711 method_order.extend(
1712 typ.methods
1713 .iter()
1714 .filter(|m| m.receiver.is_some() && !streaming_method_names.contains(&m.name)),
1715 );
1716 method_order.extend(
1717 typ.methods
1718 .iter()
1719 .filter(|m| m.receiver.is_none() && !streaming_method_names.contains(&m.name)),
1720 );
1721
1722 for method in method_order {
1723 let method_name = method.name.to_lower_camel_case();
1724 let return_type = php_type(&method.return_type);
1725 let is_void = matches!(&method.return_type, TypeRef::Unit);
1726 let is_static = method.receiver.is_none();
1727
1728 let mut doc_lines: Vec<String> = vec![];
1730 let doc_line = method.doc.lines().next().unwrap_or("").trim();
1731 if !doc_line.is_empty() {
1732 doc_lines.push(doc_line.to_string());
1733 }
1734
1735 let mut phpdoc_params: Vec<String> = vec![];
1737 for param in &method.params {
1738 if matches!(¶m.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)) {
1739 let phpdoc_type = php_phpdoc_type(¶m.ty);
1740 phpdoc_params.push(format!("@param {} ${}", phpdoc_type, param.name));
1741 }
1742 }
1743 doc_lines.extend(phpdoc_params);
1744
1745 let needs_return_phpdoc = matches!(&method.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _));
1747 if needs_return_phpdoc {
1748 let phpdoc_type = php_phpdoc_type(&method.return_type);
1749 doc_lines.push(format!("@return {phpdoc_type}"));
1750 }
1751
1752 if !doc_lines.is_empty() {
1754 content.push_str(" /**\n");
1755 for line in doc_lines {
1756 content.push_str(&format!(" * {}\n", line));
1757 }
1758 content.push_str(" */\n");
1759 }
1760
1761 let static_kw = if is_static { "static " } else { "" };
1763 let first_optional_idx = method.params.iter().position(|p| p.optional);
1764 let params: Vec<String> = method
1765 .params
1766 .iter()
1767 .enumerate()
1768 .map(|(idx, p)| {
1769 let ptype = php_type(&p.ty);
1770 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1771 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1772 format!("{nullable}{ptype} ${} = null", p.name)
1773 } else {
1774 format!("{} ${}", ptype, p.name)
1775 }
1776 })
1777 .collect();
1778 content.push_str(&format!(
1779 " public {static_kw}function {method_name}({}): {return_type}\n",
1780 params.join(", ")
1781 ));
1782 let body = if is_void {
1783 " {\n }\n"
1784 } else {
1785 " {\n throw new \\RuntimeException('Not implemented — provided by the native extension.');\n }\n"
1786 };
1787 content.push_str(body);
1788 }
1789
1790 for adapter in streaming_adapters {
1792 let item_type = adapter.item_type.as_deref().unwrap_or("array");
1793 content.push_str(&gen_php_streaming_method_wrapper(adapter, item_type));
1794 content.push('\n');
1795 }
1796
1797 content.push_str("}\n");
1798 content
1799}
1800
1801fn gen_php_streaming_method_wrapper(adapter: &alef_core::config::AdapterConfig, _item_type: &str) -> String {
1807 let method_name = adapter.name.to_lower_camel_case();
1808
1809 let mut params_vec: Vec<String> = Vec::new();
1811
1812 for p in &adapter.params {
1813 let ptype = php_type(&alef_core::ir::TypeRef::Named(p.ty.clone()));
1814 let nullable = if p.optional { "?" } else { "" };
1815 let default = if p.optional { " = null" } else { "" };
1816 params_vec.push(format!("{nullable}{ptype} ${}{default}", p.name));
1817 }
1818
1819 let params_sig = params_vec.join(", ");
1820
1821 format!(
1826 " public function {method_name}({params_sig}): \\Generator\n {{\n \
1827 throw new \\RuntimeException('Not implemented — provided by the native extension.');\n \
1828 }}\n",
1829 method_name = method_name,
1830 )
1831}
1832
1833fn php_type(ty: &TypeRef) -> String {
1835 match ty {
1836 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1837 TypeRef::Primitive(p) => match p {
1838 PrimitiveType::Bool => "bool".to_string(),
1839 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1840 PrimitiveType::U8
1841 | PrimitiveType::U16
1842 | PrimitiveType::U32
1843 | PrimitiveType::U64
1844 | PrimitiveType::I8
1845 | PrimitiveType::I16
1846 | PrimitiveType::I32
1847 | PrimitiveType::I64
1848 | PrimitiveType::Usize
1849 | PrimitiveType::Isize => "int".to_string(),
1850 },
1851 TypeRef::Optional(inner) => {
1852 let inner_type = php_type(inner);
1855 if inner_type.starts_with('?') {
1856 inner_type
1857 } else {
1858 format!("?{inner_type}")
1859 }
1860 }
1861 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1862 TypeRef::Named(name) => name.clone(),
1863 TypeRef::Unit => "void".to_string(),
1864 TypeRef::Duration => "float".to_string(),
1865 }
1866}
1867
1868fn php_property_phpdoc(var_type: &str, doc: &str, indent: &str) -> String {
1877 let doc = doc.trim();
1878 if doc.is_empty() {
1879 return format!("{indent}/** @var {var_type} */\n");
1880 }
1881 let lines: Vec<&str> = doc.lines().collect();
1882 if lines.len() == 1 {
1883 let line = lines[0].trim();
1884 return format!("{indent}/** @var {var_type} {line} */\n");
1885 }
1886 let mut out = format!("{indent}/**\n");
1888 for line in &lines {
1889 let trimmed = line.trim();
1890 if trimmed.is_empty() {
1891 out.push_str(&format!("{indent} *\n"));
1892 } else {
1893 out.push_str(&format!("{indent} * {trimmed}\n"));
1894 }
1895 }
1896 out.push_str(&format!("{indent} *\n"));
1897 out.push_str(&format!("{indent} * @var {var_type}\n"));
1898 out.push_str(&format!("{indent} */\n"));
1899 out
1900}