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::doc_emission::{self, DocTarget, sanitize_rust_idioms};
10use alef_codegen::generators::RustBindingConfig;
11use alef_codegen::generators::{self, AsyncPattern};
12use alef_codegen::naming::to_php_name;
13use alef_codegen::shared::binding_fields;
14use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
15use alef_core::config::{Language, ResolvedCrateConfig, detect_serde_available, resolve_output_dir};
16use alef_core::hash::{self, CommentStyle};
17use alef_core::ir::ApiSurface;
18use alef_core::ir::{PrimitiveType, TypeRef};
19use heck::{ToLowerCamelCase, ToPascalCase};
20use minijinja::context;
21use std::collections::HashMap;
22use std::path::PathBuf;
23
24use crate::naming::php_autoload_namespace;
25use functions::{gen_async_function_as_static_method, gen_function_as_static_method};
26
27fn sanitize_php_enum_case(name: &str) -> String {
30 if name.eq_ignore_ascii_case("class") {
31 format!("{name}_")
32 } else {
33 name.to_string()
34 }
35}
36use helpers::{gen_enum_tainted_from_binding_to_core, gen_tokio_runtime, has_enum_named_field, references_named_type};
37use types::{
38 gen_enum_constants, gen_flat_data_enum, gen_flat_data_enum_from_impls, gen_flat_data_enum_methods, gen_php_struct,
39 is_tagged_data_enum, is_untagged_data_enum,
40};
41
42pub struct PhpBackend;
43
44impl PhpBackend {
45 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
46 RustBindingConfig {
47 struct_attrs: &["php_class"],
48 field_attrs: &[],
49 struct_derives: &["Clone"],
50 method_block_attr: Some("php_impl"),
51 constructor_attr: "",
52 static_attr: None,
53 function_attr: "#[php_function]",
54 enum_attrs: &[],
55 enum_derives: &[],
56 needs_signature: false,
57 signature_prefix: "",
58 signature_suffix: "",
59 core_import,
60 async_pattern: AsyncPattern::TokioBlockOn,
61 has_serde,
62 type_name_prefix: "",
63 option_duration_on_defaults: true,
64 opaque_type_names: &[],
65 skip_impl_constructor: false,
66 cast_uints_to_i32: false,
67 cast_large_ints_to_f64: false,
68 named_non_opaque_params_by_ref: false,
69 lossy_skip_types: &[],
70 serializable_opaque_type_names: &[],
71 never_skip_cfg_field_names: &[],
72 }
73 }
74}
75
76impl Backend for PhpBackend {
77 fn name(&self) -> &str {
78 "php"
79 }
80
81 fn language(&self) -> Language {
82 Language::Php
83 }
84
85 fn capabilities(&self) -> Capabilities {
86 Capabilities {
87 supports_async: false,
88 supports_classes: true,
89 supports_enums: true,
90 supports_option: true,
91 supports_result: true,
92 ..Capabilities::default()
93 }
94 }
95
96 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
97 let data_enum_names: AHashSet<String> = api
100 .enums
101 .iter()
102 .filter(|e| is_tagged_data_enum(e))
103 .map(|e| e.name.clone())
104 .collect();
105 let untagged_data_enum_names: AHashSet<String> = api
106 .enums
107 .iter()
108 .filter(|e| is_untagged_data_enum(e))
109 .map(|e| e.name.clone())
110 .collect();
111 let enum_names: AHashSet<String> = api
114 .enums
115 .iter()
116 .filter(|e| !is_tagged_data_enum(e) && !is_untagged_data_enum(e))
117 .map(|e| e.name.clone())
118 .collect();
119 let mapper = PhpMapper {
120 enum_names: enum_names.clone(),
121 data_enum_names: data_enum_names.clone(),
122 untagged_data_enum_names: untagged_data_enum_names.clone(),
123 };
124 let core_import = config.core_import_name();
125 let lang_rename_all = config.serde_rename_all_for_language(Language::Php);
126
127 let php_config = config.php.as_ref();
129 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
130 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
131
132 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
133 let has_serde = detect_serde_available(&output_dir);
134
135 let bridge_type_aliases_php: Vec<String> = config
141 .trait_bridges
142 .iter()
143 .filter_map(|b| b.type_alias.clone())
144 .collect();
145 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
146 let mut opaque_names_vec_php: Vec<String> = api
147 .types
148 .iter()
149 .filter(|t| t.is_opaque)
150 .map(|t| t.name.clone())
151 .collect();
152 opaque_names_vec_php.extend(bridge_type_aliases_php);
153
154 let mut cfg = Self::binding_config(&core_import, has_serde);
155 cfg.opaque_type_names = &opaque_names_vec_php;
156 let never_skip_cfg_field_names: Vec<String> = config
157 .trait_bridges
158 .iter()
159 .filter_map(|b| {
160 if b.bind_via == alef_core::config::BridgeBinding::OptionsField {
161 b.resolved_options_field().map(|s| s.to_string())
162 } else {
163 None
164 }
165 })
166 .collect();
167 cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;
168
169 let mut builder = RustFileBuilder::new().with_generated_header();
171 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
172 builder.add_inner_attribute("allow(unsafe_code)");
173 builder.add_inner_attribute("allow(non_snake_case)");
175 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)");
176 builder.add_import("ext_php_rs::prelude::*");
177
178 if has_serde {
180 builder.add_import("serde_json");
181 }
182
183 for trait_path in generators::collect_trait_imports(api) {
185 builder.add_import(&trait_path);
186 }
187
188 let has_maps = api.types.iter().any(|t| {
190 t.fields
191 .iter()
192 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
193 }) || api
194 .functions
195 .iter()
196 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
197 if has_maps {
198 builder.add_import("std::collections::HashMap");
199 }
200
201 builder.add_item(
206 "#[derive(Debug, Clone, Default)]\n\
207 pub struct PhpBytes(pub Vec<u8>);\n\
208 \n\
209 impl<'a> ext_php_rs::convert::FromZval<'a> for PhpBytes {\n \
210 const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::String;\n \
211 fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {\n \
212 zval.zend_str().map(|zs| PhpBytes(zs.as_bytes().to_vec()))\n \
213 }\n\
214 }\n\
215 \n\
216 impl From<PhpBytes> for Vec<u8> {\n \
217 fn from(b: PhpBytes) -> Self { b.0 }\n\
218 }\n\
219 \n\
220 impl From<Vec<u8>> for PhpBytes {\n \
221 fn from(v: Vec<u8>) -> Self { PhpBytes(v) }\n\
222 }\n",
223 );
224
225 let custom_mods = config.custom_modules.for_language(Language::Php);
227 for module in custom_mods {
228 builder.add_item(&format!("pub mod {module};"));
229 }
230
231 let has_async =
233 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
234
235 if has_async {
236 builder.add_item(&gen_tokio_runtime());
237 }
238
239 let opaque_types: AHashSet<String> = api
241 .types
242 .iter()
243 .filter(|t| t.is_opaque)
244 .map(|t| t.name.clone())
245 .collect();
246 if !opaque_types.is_empty() {
247 builder.add_import("std::sync::Arc");
248 }
249
250 let mutex_types: AHashSet<String> = api
252 .types
253 .iter()
254 .filter(|t| t.is_opaque && alef_codegen::generators::type_needs_mutex(t))
255 .map(|t| t.name.clone())
256 .collect();
257 if !mutex_types.is_empty() {
258 builder.add_import("std::sync::Mutex");
259 }
260
261 let extension_name = config.php_extension_name();
264 let php_namespace = php_autoload_namespace(config);
265
266 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
268
269 let streaming_method_keys: AHashSet<String> = config
274 .adapters
275 .iter()
276 .filter(|a| matches!(a.pattern, alef_core::config::AdapterPattern::Streaming))
277 .filter_map(|a| a.owner_type.as_deref().map(|owner| format!("{owner}.{}", a.name)))
278 .collect();
279
280 for adapter in &config.adapters {
282 match adapter.pattern {
283 alef_core::config::AdapterPattern::Streaming => {
284 let key = alef_adapters::stream_struct_key(adapter);
285 if let Some(struct_code) = adapter_bodies.get(&key) {
286 builder.add_item(struct_code);
287 }
288 }
289 alef_core::config::AdapterPattern::CallbackBridge => {
290 let struct_key = format!("{}.__bridge_struct__", adapter.name);
291 let impl_key = format!("{}.__bridge_impl__", adapter.name);
292 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
293 builder.add_item(struct_code);
294 }
295 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
296 builder.add_item(impl_code);
297 }
298 }
299 _ => {}
300 }
301 }
302
303 for typ in api
304 .types
305 .iter()
306 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
307 {
308 if typ.is_opaque {
309 let ns_escaped = php_namespace.replace('\\', "\\\\");
313 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
314 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
315 let opaque_cfg = RustBindingConfig {
316 struct_attrs: &opaque_attr_arr,
317 ..cfg
318 };
319 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
320 builder.add_item(&types::gen_opaque_struct_methods_with_exclude(
321 typ,
322 &mapper,
323 &opaque_types,
324 &core_import,
325 &adapter_bodies,
326 &mutex_types,
327 &streaming_method_keys,
328 &config.trait_bridges,
329 ));
330 if let Some(ctor) = config.client_constructors.get(&typ.name) {
332 let ctor_body = generators::gen_opaque_constructor(ctor, &typ.name, &core_import, "#[php_method]");
333 let ctor_impl = format!("#[php_impl]\nimpl {} {{\n{}}}", typ.name, ctor_body);
334 builder.add_item(&ctor_impl);
335 }
336 } else {
337 builder.add_item(&gen_php_struct(
340 typ,
341 &mapper,
342 &cfg,
343 Some(&php_namespace),
344 &enum_names,
345 &lang_rename_all,
346 ));
347 builder.add_item(&types::gen_struct_methods_with_exclude(
348 typ,
349 &mapper,
350 has_serde,
351 &core_import,
352 &opaque_types,
353 &enum_names,
354 &api.enums,
355 &exclude_functions,
356 &bridge_type_aliases_set,
357 &never_skip_cfg_field_names,
358 &mutex_types,
359 ));
360 }
361 }
362
363 for enum_def in &api.enums {
364 if is_tagged_data_enum(enum_def) {
365 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
367 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
368 } else {
369 builder.add_item(&gen_enum_constants(enum_def));
370 }
371 }
372
373 let included_functions: Vec<_> = api
378 .functions
379 .iter()
380 .filter(|f| !exclude_functions.contains(&f.name))
381 .collect();
382 if !included_functions.is_empty() {
383 let facade_class_name = extension_name.to_pascal_case();
384 let mut method_items: Vec<String> = Vec::new();
387 for func in included_functions {
388 if alef_codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&func.name, &config.trait_bridges)
389 {
390 continue;
391 }
392 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
393 if let Some((param_idx, bridge_cfg)) = bridge_param {
394 let bridge_handle_path = bridge_handle_path(api, bridge_cfg, &core_import);
395 method_items.push(crate::trait_bridge::gen_bridge_function(
396 func,
397 param_idx,
398 bridge_cfg,
399 &mapper,
400 &opaque_types,
401 &core_import,
402 &bridge_handle_path,
403 ));
404 } else if func.is_async {
405 method_items.push(gen_async_function_as_static_method(
406 func,
407 &mapper,
408 &opaque_types,
409 &core_import,
410 &config.trait_bridges,
411 &mutex_types,
412 ));
413 } else {
414 method_items.push(gen_function_as_static_method(
415 func,
416 &mapper,
417 &opaque_types,
418 &core_import,
419 &config.trait_bridges,
420 has_serde,
421 &mutex_types,
422 ));
423 }
424 }
425
426 for adapter in &config.adapters {
428 if !matches!(adapter.pattern, alef_core::config::AdapterPattern::Streaming) {
429 continue;
430 }
431 if adapter.owner_type.is_none() {
432 continue;
433 }
434 method_items.push(gen_streaming_adapter_facade_method(
435 adapter,
436 &mapper,
437 &opaque_types,
438 &core_import,
439 ));
440 }
441
442 for bridge_cfg in &config.trait_bridges {
444 if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
445 method_items.push(format!(
446 "pub fn {}(backend: &mut ext_php_rs::types::ZendObject) -> ext_php_rs::prelude::PhpResult<()> {{\n \
447 crate::{}(backend)\n}}",
448 register_fn,
449 register_fn
450 ));
451 }
452 if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
453 method_items.push(format!(
454 "pub fn {}(name: String) -> ext_php_rs::prelude::PhpResult<()> {{\n \
455 crate::{}(name)\n}}",
456 unregister_fn, unregister_fn
457 ));
458 }
459 if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
460 method_items.push(format!(
461 "pub fn {}() -> ext_php_rs::prelude::PhpResult<()> {{\n \
462 crate::{}()\n}}",
463 clear_fn, clear_fn
464 ));
465 }
466 }
467
468 let methods_joined = method_items
469 .iter()
470 .map(|m| {
471 m.lines()
473 .map(|l| {
474 if l.is_empty() {
475 String::new()
476 } else {
477 format!(" {l}")
478 }
479 })
480 .collect::<Vec<_>>()
481 .join("\n")
482 })
483 .collect::<Vec<_>>()
484 .join("\n\n");
485 let php_api_class_name = format!("{facade_class_name}Api");
488 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
490 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
491 let facade_struct = format!(
492 "#[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}}"
493 );
494 builder.add_item(&facade_struct);
495
496 for bridge_cfg in &config.trait_bridges {
498 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
499 let bridge = crate::trait_bridge::gen_trait_bridge(
500 trait_type,
501 bridge_cfg,
502 &core_import,
503 &config.error_type_name(),
504 &config.error_constructor_expr(),
505 api,
506 );
507 for imp in &bridge.imports {
508 builder.add_import(imp);
509 }
510 builder.add_item(&bridge.code);
511 }
512 }
513 }
514
515 let convertible = alef_codegen::conversions::convertible_types(api);
516 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
517 let input_types = alef_codegen::conversions::input_type_names(api);
518 let enum_names_ref = &mapper.enum_names;
523 let bridge_skip_types: Vec<String> = config
524 .trait_bridges
525 .iter()
526 .filter(|b| !matches!(b.bind_via, alef_core::config::BridgeBinding::OptionsField))
527 .filter_map(|b| b.type_alias.clone())
528 .collect();
529 let trait_bridge_arc_wrapper_field_names: Vec<String> = config
534 .trait_bridges
535 .iter()
536 .filter(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField)
537 .filter_map(|b| b.resolved_options_field().map(String::from))
538 .collect();
539 let mut conv_opaque_types: AHashSet<String> = opaque_types.clone();
544 for bridge in &config.trait_bridges {
545 if let Some(alias) = &bridge.type_alias {
546 conv_opaque_types.insert(alias.clone());
547 }
548 }
549 let php_conv_config = ConversionConfig {
550 cast_large_ints_to_i64: true,
551 enum_string_names: Some(enum_names_ref),
552 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
553 json_as_value: true,
557 include_cfg_metadata: false,
558 option_duration_on_defaults: true,
559 from_binding_skip_types: &bridge_skip_types,
560 never_skip_cfg_field_names: &never_skip_cfg_field_names,
561 opaque_types: Some(&conv_opaque_types),
562 trait_bridge_arc_wrapper_field_names: &trait_bridge_arc_wrapper_field_names,
563 ..Default::default()
564 };
565 let mut enum_tainted: AHashSet<String> = AHashSet::new();
567 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
568 if has_enum_named_field(typ, enum_names_ref) {
569 enum_tainted.insert(typ.name.clone());
570 }
571 }
572 let mut changed = true;
574 while changed {
575 changed = false;
576 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
577 if !enum_tainted.contains(&typ.name)
578 && binding_fields(&typ.fields).any(|f| references_named_type(&f.ty, &enum_tainted))
579 {
580 enum_tainted.insert(typ.name.clone());
581 changed = true;
582 }
583 }
584 }
585 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
586 if input_types.contains(&typ.name)
588 && !enum_tainted.contains(&typ.name)
589 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
590 {
591 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
592 typ,
593 &core_import,
594 &php_conv_config,
595 ));
596 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
597 builder.add_item(&gen_enum_tainted_from_binding_to_core(
604 typ,
605 &core_import,
606 enum_names_ref,
607 &enum_tainted,
608 &php_conv_config,
609 &api.enums,
610 &bridge_type_aliases_set,
611 ));
612 }
613 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
615 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
616 typ,
617 &core_import,
618 &opaque_types,
619 &php_conv_config,
620 ));
621 }
622 }
623
624 let mut emitted_binding_to_core: AHashSet<String> = api
634 .types
635 .iter()
636 .filter(|typ| !typ.is_trait && input_types.contains(&typ.name))
637 .filter(|typ| {
638 (enum_tainted.contains(&typ.name))
639 || alef_codegen::conversions::can_generate_conversion(typ, &convertible)
640 })
641 .map(|typ| typ.name.clone())
642 .collect();
643 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
644 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
645 for variant in &enum_def.variants {
648 for field in &variant.fields {
649 if let TypeRef::Named(type_name) = &field.ty {
650 if let Some(typ) = api.types.iter().find(|t| &t.name == type_name) {
651 if emitted_binding_to_core.contains(&typ.name) {
652 continue;
653 }
654 if enum_tainted.contains(&typ.name) {
655 builder.add_item(&gen_enum_tainted_from_binding_to_core(
656 typ,
657 &core_import,
658 enum_names_ref,
659 &enum_tainted,
660 &php_conv_config,
661 &api.enums,
662 &bridge_type_aliases_set,
663 ));
664 emitted_binding_to_core.insert(typ.name.clone());
665 } else if alef_codegen::conversions::can_generate_conversion(typ, &convertible) {
666 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
667 typ,
668 &core_import,
669 &php_conv_config,
670 ));
671 emitted_binding_to_core.insert(typ.name.clone());
672 }
673 }
674 }
675 }
676 }
677 }
678
679 for typ in api.types.iter().filter(|t| !t.is_trait) {
683 if !emitted_binding_to_core.contains(&typ.name) {
684 if enum_tainted.contains(&typ.name) {
685 builder.add_item(&gen_enum_tainted_from_binding_to_core(
686 typ,
687 &core_import,
688 enum_names_ref,
689 &enum_tainted,
690 &php_conv_config,
691 &api.enums,
692 &bridge_type_aliases_set,
693 ));
694 emitted_binding_to_core.insert(typ.name.clone());
695 } else if alef_codegen::conversions::can_generate_conversion(typ, &convertible) {
696 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
697 typ,
698 &core_import,
699 &php_conv_config,
700 ));
701 emitted_binding_to_core.insert(typ.name.clone());
702 }
703 }
704 }
705
706 for error in &api.errors {
708 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
709 let methods_impl = alef_codegen::error_gen::gen_php_error_methods_impl(error, &core_import);
711 if !methods_impl.is_empty() {
712 builder.add_item(&methods_impl);
713 }
714 }
715
716 if has_serde {
720 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
721 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
722 pub fn max_compression_ratio() -> i64 { 100 }\n\
723 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
724 pub fn max_nesting_depth() -> i64 { 1024 }\n\
725 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
726 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
727 pub fn max_iterations() -> i64 { 10_000_000 }\n\
728 pub fn max_xml_depth() -> i64 { 1024 }\n\
729 pub fn max_table_cells() -> i64 { 100_000 }\n\
730 }";
731 builder.add_item(serde_module);
732 }
733
734 let php_config = config.php.as_ref();
740 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
741
742 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
746 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
747 }
748
749 let mut class_registrations = String::new();
752 for typ in api
753 .types
754 .iter()
755 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
756 {
757 class_registrations.push_str(&crate::template_env::render(
758 "php_class_registration.jinja",
759 context! { class_name => &typ.name },
760 ));
761 }
762 if !api.functions.is_empty() {
764 let facade_class_name = extension_name.to_pascal_case();
765 class_registrations.push_str(&crate::template_env::render(
766 "php_class_registration.jinja",
767 context! { class_name => &format!("{facade_class_name}Api") },
768 ));
769 }
770 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
773 class_registrations.push_str(&crate::template_env::render(
774 "php_class_registration.jinja",
775 context! { class_name => &enum_def.name },
776 ));
777 }
778 for error in api.errors.iter().filter(|e| !e.methods.is_empty()) {
780 let info_class = format!("{}Info", error.name);
781 class_registrations.push_str(&crate::template_env::render(
782 "php_class_registration.jinja",
783 context! { class_name => &info_class },
784 ));
785 }
786 builder.add_item(&format!(
787 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
788 ));
789
790 let mut content = builder.build();
791
792 for bridge in &config.trait_bridges {
797 if let Some(field_name) = bridge.resolved_options_field() {
798 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
799 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
800 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
801 let builder_type = format!("{}Builder", options_type);
802 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
803 let bridge_handle_path = bridge_handle_path(api, bridge, &core_import);
804
805 let old_method = format!(
811 " 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 }}"
812 );
813 let new_method = format!(
814 " 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 }}"
815 );
816
817 content = content.replace(&old_method, &new_method);
818 }
819 }
820
821 let php_stubs_dir = config
824 .php
825 .as_ref()
826 .and_then(|p| p.stubs.as_ref())
827 .map(|s| s.output.to_string_lossy().to_string())
828 .unwrap_or_else(|| "packages/php/src/".to_string());
829
830 let php_namespace = php_autoload_namespace(config);
831
832 let mut generated_files = vec![GeneratedFile {
833 path: PathBuf::from(&output_dir).join("lib.rs"),
834 content,
835 generated_header: false,
836 }];
837
838 for bridge_cfg in &config.trait_bridges {
840 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
841 let is_visitor_bridge = bridge_cfg.type_alias.is_some()
843 && bridge_cfg.register_fn.is_none()
844 && bridge_cfg.super_trait.is_none()
845 && trait_type.methods.iter().all(|m| m.has_default_impl);
846
847 if is_visitor_bridge {
848 let interface_content = crate::trait_bridge::gen_visitor_interface(
849 trait_type,
850 bridge_cfg,
851 &php_namespace,
852 &HashMap::new(), );
854 let interface_filename = format!("{}Interface.php", bridge_cfg.trait_name);
855 generated_files.push(GeneratedFile {
856 path: PathBuf::from(&php_stubs_dir).join(&interface_filename),
857 content: interface_content,
858 generated_header: false,
859 });
860 }
861 }
862 }
863
864 Ok(generated_files)
865 }
866
867 fn generate_public_api(
868 &self,
869 api: &ApiSurface,
870 config: &ResolvedCrateConfig,
871 ) -> anyhow::Result<Vec<GeneratedFile>> {
872 let escape_phpdoc_line = |s: &str| s.replace("*/", "* /");
874
875 let extension_name = config.php_extension_name();
876 let class_name = extension_name.to_pascal_case();
877
878 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(&crate::template_env::render(
886 "php_declare_strict_types.jinja",
887 minijinja::Value::default(),
888 ));
889 content.push('\n');
891
892 let namespace = php_autoload_namespace(config);
894
895 content.push_str(&crate::template_env::render(
896 "php_namespace.jinja",
897 context! { namespace => &namespace },
898 ));
899 content.push('\n');
901 content.push_str(&crate::template_env::render(
902 "php_facade_class_declaration.jinja",
903 context! { class_name => &class_name },
904 ));
905
906 let bridge_param_names_pub: ahash::AHashSet<&str> = config
908 .trait_bridges
909 .iter()
910 .filter_map(|b| b.param_name.as_deref())
911 .collect();
912
913 let no_arg_constructor_types: AHashSet<String> = api
918 .types
919 .iter()
920 .filter(|t| t.fields.iter().all(|f| f.optional))
921 .map(|t| t.name.clone())
922 .collect();
923
924 for func in &api.functions {
926 if alef_codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&func.name, &config.trait_bridges) {
930 continue;
931 }
932 let method_name = func.name.to_lower_camel_case();
937 let return_php_type = php_type(&func.return_type);
938
939 let visible_params: Vec<_> = func
941 .params
942 .iter()
943 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
944 .collect();
945
946 content.push_str(&crate::template_env::render(
948 "php_phpdoc_block_start.jinja",
949 minijinja::Value::default(),
950 ));
951 if func.doc.is_empty() {
952 content.push_str(&crate::template_env::render(
953 "php_phpdoc_text_line.jinja",
954 context! { text => &format!("{}.", method_name) },
955 ));
956 } else {
957 let sections = doc_emission::parse_rustdoc_sections(&func.doc);
959 for line in sections.summary.lines() {
961 content.push_str(" * ");
962 content.push_str(&escape_phpdoc_line(line));
963 content.push('\n');
964 }
965 }
968 content.push_str(&crate::template_env::render(
969 "php_phpdoc_empty_line.jinja",
970 minijinja::Value::default(),
971 ));
972 for p in &visible_params {
973 let ptype = php_phpdoc_type(&p.ty);
974 let nullable_prefix = if p.optional { "?" } else { "" };
975 content.push_str(&crate::template_env::render(
976 "php_phpdoc_param_line.jinja",
977 context! {
978 nullable_prefix => nullable_prefix,
979 param_type => &ptype,
980 param_name => &p.name,
981 },
982 ));
983 }
984 let return_phpdoc = php_phpdoc_type(&func.return_type);
985 content.push_str(&crate::template_env::render(
986 "php_phpdoc_return_line.jinja",
987 context! { return_type => &return_phpdoc },
988 ));
989 if func.error_type.is_some() {
990 content.push_str(&crate::template_env::render(
991 "php_phpdoc_throws_line.jinja",
992 context! {
993 namespace => namespace.as_str(),
994 class_name => &class_name,
995 },
996 ));
997 }
998 content.push_str(&crate::template_env::render(
999 "php_phpdoc_block_end.jinja",
1000 minijinja::Value::default(),
1001 ));
1002
1003 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
1014 if let TypeRef::Named(name) = &p.ty {
1015 (name.ends_with("Config") || name.as_str() == "config")
1016 && no_arg_constructor_types.contains(name.as_str())
1017 } else {
1018 false
1019 }
1020 };
1021
1022 let mut first_optional_idx = None;
1023 for (idx, p) in visible_params.iter().enumerate() {
1024 if p.optional || is_optional_config_param(p) {
1025 first_optional_idx = Some(idx);
1026 break;
1027 }
1028 }
1029
1030 content.push_str(&crate::template_env::render(
1031 "php_method_signature_start.jinja",
1032 context! { method_name => &method_name },
1033 ));
1034
1035 let params: Vec<String> = visible_params
1036 .iter()
1037 .enumerate()
1038 .map(|(idx, p)| {
1039 let ptype = php_type(&p.ty);
1040 let should_be_optional = p.optional
1045 || is_optional_config_param(p)
1046 || first_optional_idx.is_some_and(|first| idx >= first);
1047 if should_be_optional {
1048 format!("?{} ${} = null", ptype, p.name)
1049 } else {
1050 format!("{} ${}", ptype, p.name)
1051 }
1052 })
1053 .collect();
1054 content.push_str(¶ms.join(", "));
1055 content.push_str(&crate::template_env::render(
1056 "php_method_signature_end.jinja",
1057 context! { return_type => &return_php_type },
1058 ));
1059 let ext_method_name = func.name.to_lower_camel_case();
1064 let is_void = matches!(&func.return_type, TypeRef::Unit);
1065 let call_params = visible_params
1073 .iter()
1074 .enumerate()
1075 .map(|(idx, p)| {
1076 let should_be_optional = p.optional
1077 || is_optional_config_param(p)
1078 || first_optional_idx.is_some_and(|first| idx >= first);
1079 if should_be_optional && is_optional_config_param(p) {
1080 if let TypeRef::Named(type_name) = &p.ty {
1081 return format!("${} ?? new {}()", p.name, type_name);
1082 }
1083 }
1084 format!("${}", p.name)
1085 })
1086 .collect::<Vec<_>>()
1087 .join(", ");
1088 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
1089 if is_void {
1090 content.push_str(&crate::template_env::render(
1091 "php_method_call_statement.jinja",
1092 context! { call_expr => &call_expr },
1093 ));
1094 } else {
1095 content.push_str(&crate::template_env::render(
1096 "php_method_call_return.jinja",
1097 context! { call_expr => &call_expr },
1098 ));
1099 }
1100 content.push_str(&crate::template_env::render(
1101 "php_method_end.jinja",
1102 minijinja::Value::default(),
1103 ));
1104 }
1105
1106 for bridge_cfg in &config.trait_bridges {
1108 if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
1109 let method_name = register_fn.to_lower_camel_case();
1110 content.push_str(&crate::template_env::render(
1111 "php_phpdoc_block_start.jinja",
1112 minijinja::Value::default(),
1113 ));
1114 content.push_str(&crate::template_env::render(
1115 "php_phpdoc_text_line.jinja",
1116 context! { text => &format!("{}.", method_name) },
1117 ));
1118 content.push_str(&crate::template_env::render(
1119 "php_phpdoc_empty_line.jinja",
1120 minijinja::Value::default(),
1121 ));
1122 let interface_name = &bridge_cfg.trait_name;
1123 content.push_str(&crate::template_env::render(
1124 "php_phpdoc_param_line.jinja",
1125 context! {
1126 nullable_prefix => "",
1127 param_type => interface_name,
1128 param_name => "backend",
1129 },
1130 ));
1131 content.push_str(&crate::template_env::render(
1132 "php_phpdoc_return_line.jinja",
1133 context! { return_type => "void" },
1134 ));
1135 content.push_str(&crate::template_env::render(
1136 "php_phpdoc_block_end.jinja",
1137 minijinja::Value::default(),
1138 ));
1139 content.push_str(&crate::template_env::render(
1140 "php_method_signature_start.jinja",
1141 context! { method_name => &method_name },
1142 ));
1143 content.push_str(&format!("{} $backend = null) : void\n {{\n", interface_name));
1144 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}($backend)");
1145 content.push_str(&crate::template_env::render(
1146 "php_method_call_statement.jinja",
1147 context! { call_expr => &call_expr },
1148 ));
1149 content.push_str(&crate::template_env::render(
1150 "php_method_end.jinja",
1151 minijinja::Value::default(),
1152 ));
1153 }
1154 if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
1155 let method_name = unregister_fn.to_lower_camel_case();
1156 content.push_str(&crate::template_env::render(
1157 "php_phpdoc_block_start.jinja",
1158 minijinja::Value::default(),
1159 ));
1160 content.push_str(&crate::template_env::render(
1161 "php_phpdoc_text_line.jinja",
1162 context! { text => &format!("{}.", method_name) },
1163 ));
1164 content.push_str(&crate::template_env::render(
1165 "php_phpdoc_empty_line.jinja",
1166 minijinja::Value::default(),
1167 ));
1168 content.push_str(&crate::template_env::render(
1169 "php_phpdoc_param_line.jinja",
1170 context! {
1171 nullable_prefix => "",
1172 param_type => "string",
1173 param_name => "name",
1174 },
1175 ));
1176 content.push_str(&crate::template_env::render(
1177 "php_phpdoc_return_line.jinja",
1178 context! { return_type => "void" },
1179 ));
1180 content.push_str(&crate::template_env::render(
1181 "php_phpdoc_block_end.jinja",
1182 minijinja::Value::default(),
1183 ));
1184 content.push_str(&crate::template_env::render(
1185 "php_method_signature_start.jinja",
1186 context! { method_name => &method_name },
1187 ));
1188 content.push_str("string $name) : void\n {\n");
1189 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}($name)");
1190 content.push_str(&crate::template_env::render(
1191 "php_method_call_statement.jinja",
1192 context! { call_expr => &call_expr },
1193 ));
1194 content.push_str(&crate::template_env::render(
1195 "php_method_end.jinja",
1196 minijinja::Value::default(),
1197 ));
1198 }
1199 if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
1200 let method_name = clear_fn.to_lower_camel_case();
1201 content.push_str(&crate::template_env::render(
1202 "php_phpdoc_block_start.jinja",
1203 minijinja::Value::default(),
1204 ));
1205 content.push_str(&crate::template_env::render(
1206 "php_phpdoc_text_line.jinja",
1207 context! { text => &format!("{}.", method_name) },
1208 ));
1209 content.push_str(&crate::template_env::render(
1210 "php_phpdoc_empty_line.jinja",
1211 minijinja::Value::default(),
1212 ));
1213 content.push_str(&crate::template_env::render(
1214 "php_phpdoc_return_line.jinja",
1215 context! { return_type => "void" },
1216 ));
1217 content.push_str(&crate::template_env::render(
1218 "php_phpdoc_block_end.jinja",
1219 minijinja::Value::default(),
1220 ));
1221 content.push_str(&crate::template_env::render(
1222 "php_method_signature_start.jinja",
1223 context! { method_name => &method_name },
1224 ));
1225 content.push_str(") : void\n {\n");
1226 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}()");
1227 content.push_str(&crate::template_env::render(
1228 "php_method_call_statement.jinja",
1229 context! { call_expr => &call_expr },
1230 ));
1231 content.push_str(&crate::template_env::render(
1232 "php_method_end.jinja",
1233 minijinja::Value::default(),
1234 ));
1235 }
1236 }
1237
1238 content.push_str(&crate::template_env::render(
1239 "php_class_end.jinja",
1240 minijinja::Value::default(),
1241 ));
1242
1243 let output_dir = config
1247 .php
1248 .as_ref()
1249 .and_then(|p| p.stubs.as_ref())
1250 .map(|s| s.output.to_string_lossy().to_string())
1251 .unwrap_or_else(|| "packages/php/src/".to_string());
1252
1253 let mut files: Vec<GeneratedFile> = Vec::new();
1254 files.push(GeneratedFile {
1255 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
1256 content,
1257 generated_header: false,
1258 });
1259
1260 for typ in api.types.iter().filter(|t| t.is_opaque && !t.is_trait) {
1266 let streaming_adapters: Vec<&alef_core::config::AdapterConfig> = config
1267 .adapters
1268 .iter()
1269 .filter(|a| {
1270 matches!(a.pattern, alef_core::config::AdapterPattern::Streaming)
1271 && a.owner_type.as_deref() == Some(&typ.name)
1272 && !a.skip_languages.iter().any(|l| l == "php")
1273 })
1274 .collect();
1275 let streaming_method_names: AHashSet<String> = streaming_adapters.iter().map(|a| a.name.clone()).collect();
1276 let opaque_file = gen_php_opaque_class_file(
1277 typ,
1278 &namespace,
1279 &streaming_adapters,
1280 &streaming_method_names,
1281 &config.trait_bridges,
1282 );
1283 files.push(GeneratedFile {
1284 path: PathBuf::from(&output_dir).join(format!("{}.php", typ.name)),
1285 content: opaque_file,
1286 generated_header: false,
1287 });
1288 }
1289
1290 Ok(files)
1291 }
1292
1293 fn generate_type_stubs(
1294 &self,
1295 api: &ApiSurface,
1296 config: &ResolvedCrateConfig,
1297 ) -> anyhow::Result<Vec<GeneratedFile>> {
1298 let extension_name = config.php_extension_name();
1299 let class_name = extension_name.to_pascal_case();
1300
1301 let namespace = php_autoload_namespace(config);
1303
1304 let mut content = String::new();
1309 content.push_str(&crate::template_env::render(
1310 "php_file_header.jinja",
1311 minijinja::Value::default(),
1312 ));
1313 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1314 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
1315 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
1316 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
1317 content.push_str(&crate::template_env::render(
1318 "php_declare_strict_types.jinja",
1319 minijinja::Value::default(),
1320 ));
1321 content.push('\n');
1323 content.push_str(&crate::template_env::render(
1325 "php_namespace_block_begin.jinja",
1326 context! { namespace => &namespace },
1327 ));
1328
1329 content.push_str(&crate::template_env::render(
1331 "php_exception_class_declaration.jinja",
1332 context! { class_name => &class_name },
1333 ));
1334 content.push_str(
1335 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1336 );
1337 let has_status_code = api
1340 .errors
1341 .iter()
1342 .any(|e| e.methods.iter().any(|m| m.name == "status_code"));
1343 let has_is_transient = api
1344 .errors
1345 .iter()
1346 .any(|e| e.methods.iter().any(|m| m.name == "is_transient"));
1347 let has_error_type = api
1348 .errors
1349 .iter()
1350 .any(|e| e.methods.iter().any(|m| m.name == "error_type"));
1351 if has_status_code {
1352 content.push_str(
1353 " /** HTTP status code for this error (0 means no associated status). */\n \
1354 public function statusCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1355 );
1356 }
1357 if has_is_transient {
1358 content.push_str(
1359 " /** Returns true if the error is transient and a retry may succeed. */\n \
1360 public function isTransient(): bool { throw new \\RuntimeException('Not implemented.'); }\n",
1361 );
1362 }
1363 if has_error_type {
1364 content.push_str(
1365 " /** Machine-readable error category string for matching and logging. */\n \
1366 public function errorType(): string { throw new \\RuntimeException('Not implemented.'); }\n",
1367 );
1368 }
1369 content.push_str("}\n\n");
1370
1371 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
1378 if typ.is_opaque || typ.fields.is_empty() {
1379 continue;
1380 }
1381 if !typ.doc.is_empty() {
1382 content.push_str("/**\n");
1383 let sanitized = sanitize_rust_idioms(&typ.doc, DocTarget::PhpDoc);
1384 content.push_str(&crate::template_env::render(
1385 "php_phpdoc_lines.jinja",
1386 context! {
1387 doc_lines => sanitized.lines().collect::<Vec<_>>(),
1388 indent => "",
1389 },
1390 ));
1391 content.push_str(" */\n");
1392 }
1393 content.push_str(&crate::template_env::render(
1394 "php_record_class_stub_declaration.jinja",
1395 context! { class_name => &typ.name },
1396 ));
1397
1398 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = binding_fields(&typ.fields).collect();
1401 sorted_fields.sort_by_key(|f| f.optional);
1402
1403 let params: Vec<String> = sorted_fields
1408 .iter()
1409 .map(|f| {
1410 let ptype = php_type(&f.ty);
1411 let nullable = if f.optional && !ptype.starts_with('?') {
1412 format!("?{ptype}")
1413 } else {
1414 ptype
1415 };
1416 let default = if f.optional { " = null" } else { "" };
1417 let php_name = to_php_name(&f.name);
1418 let phpdoc_type = php_phpdoc_type(&f.ty);
1419 let var_type = if f.optional && !phpdoc_type.starts_with('?') {
1420 format!("?{phpdoc_type}")
1421 } else {
1422 phpdoc_type
1423 };
1424 let phpdoc = php_property_phpdoc(&var_type, &f.doc, " ");
1425 format!("{phpdoc} public readonly {nullable} ${php_name}{default}",)
1426 })
1427 .collect();
1428 content.push_str(&crate::template_env::render(
1429 "php_constructor_method.jinja",
1430 context! { params => ¶ms.join(",\n") },
1431 ));
1432
1433 let non_excluded_methods: Vec<&alef_core::ir::MethodDef> = typ
1438 .methods
1439 .iter()
1440 .filter(|m| !m.binding_excluded && !m.sanitized)
1441 .collect();
1442 for method in non_excluded_methods {
1443 let method_name = method.name.to_lower_camel_case();
1444 let is_static = method.receiver.is_none();
1445 let return_type = php_type(&method.return_type);
1446 let first_optional_idx = method.params.iter().position(|p| p.optional);
1447 let params: Vec<String> = method
1448 .params
1449 .iter()
1450 .enumerate()
1451 .map(|(idx, p)| {
1452 let ptype = php_type(&p.ty);
1453 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1454 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1455 format!("{nullable}{ptype} ${} = null", p.name)
1456 } else {
1457 format!("{} ${}", ptype, p.name)
1458 }
1459 })
1460 .collect();
1461 let static_kw = if is_static { "static " } else { "" };
1462 let is_void = matches!(&method.return_type, TypeRef::Unit);
1463 let stub_body = if is_void {
1464 "{ }".to_string()
1465 } else {
1466 "{ throw new \\RuntimeException('Not implemented — provided by the native extension.'); }"
1467 .to_string()
1468 };
1469 content.push_str(&format!(
1470 " public {static_kw}function {method_name}({}): {return_type}\n {stub_body}\n",
1471 params.join(", ")
1472 ));
1473 }
1474
1475 content.push_str("}\n\n");
1476 }
1477
1478 for enum_def in &api.enums {
1481 if is_tagged_data_enum(enum_def) {
1482 if !enum_def.doc.is_empty() {
1484 content.push_str("/**\n");
1485 let sanitized = sanitize_rust_idioms(&enum_def.doc, DocTarget::PhpDoc);
1486 content.push_str(&crate::template_env::render(
1487 "php_phpdoc_lines.jinja",
1488 context! {
1489 doc_lines => sanitized.lines().collect::<Vec<_>>(),
1490 indent => "",
1491 },
1492 ));
1493 content.push_str(" */\n");
1494 }
1495 content.push_str(&crate::template_env::render(
1496 "php_record_class_stub_declaration.jinja",
1497 context! { class_name => &enum_def.name },
1498 ));
1499 content.push_str("}\n\n");
1500 } else {
1501 content.push_str(&crate::template_env::render(
1503 "php_tagged_enum_declaration.jinja",
1504 context! { enum_name => &enum_def.name },
1505 ));
1506 for variant in &enum_def.variants {
1507 let case_name = sanitize_php_enum_case(&variant.name);
1508 content.push_str(&crate::template_env::render(
1509 "php_enum_variant_stub.jinja",
1510 context! {
1511 variant_name => case_name,
1512 value => &variant.name,
1513 },
1514 ));
1515 }
1516 content.push_str("}\n\n");
1517 }
1518 }
1519
1520 if !api.functions.is_empty() {
1525 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1527 .trait_bridges
1528 .iter()
1529 .filter_map(|b| b.param_name.as_deref())
1530 .collect();
1531
1532 content.push_str(&crate::template_env::render(
1533 "php_api_class_declaration.jinja",
1534 context! { class_name => &class_name },
1535 ));
1536 for func in &api.functions {
1537 let return_type = php_type_fq(&func.return_type, &namespace);
1538 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1539 let visible_params: Vec<_> = func
1541 .params
1542 .iter()
1543 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1544 .collect();
1545 let has_array_params = visible_params
1552 .iter()
1553 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1554 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1555 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1556 let first_optional_idx = visible_params.iter().position(|p| p.optional);
1557 if has_array_params || has_array_return {
1558 content.push_str(" /**\n");
1559 for (idx, p) in visible_params.iter().enumerate() {
1560 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1561 let nullable_prefix = if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1562 "?"
1563 } else {
1564 ""
1565 };
1566 content.push_str(&crate::template_env::render(
1567 "php_phpdoc_static_param.jinja",
1568 context! {
1569 nullable_prefix => nullable_prefix,
1570 ptype => &ptype,
1571 param_name => &p.name,
1572 },
1573 ));
1574 }
1575 content.push_str(&crate::template_env::render(
1576 "php_phpdoc_static_return.jinja",
1577 context! { return_phpdoc => &return_phpdoc },
1578 ));
1579 content.push_str(" */\n");
1580 }
1581 let params: Vec<String> = visible_params
1582 .iter()
1583 .enumerate()
1584 .map(|(idx, p)| {
1585 let ptype = php_type_fq(&p.ty, &namespace);
1586 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1587 let nullable_ptype = if ptype.starts_with('?') {
1588 ptype
1589 } else {
1590 format!("?{ptype}")
1591 };
1592 format!("{} ${} = null", nullable_ptype, p.name)
1593 } else {
1594 format!("{} ${}", ptype, p.name)
1595 }
1596 })
1597 .collect();
1598 let stub_method_name = func.name.to_lower_camel_case();
1602 let is_void_stub = return_type == "void";
1603 let stub_body = if is_void_stub {
1604 "{ }".to_string()
1605 } else {
1606 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1607 };
1608 content.push_str(&crate::template_env::render(
1609 "php_static_method_stub.jinja",
1610 context! {
1611 method_name => &stub_method_name,
1612 params => ¶ms.join(", "),
1613 return_type => &return_type,
1614 stub_body => &stub_body,
1615 },
1616 ));
1617 }
1618 content.push_str("}\n\n");
1619 }
1620
1621 content.push_str(&crate::template_env::render(
1623 "php_namespace_block_end.jinja",
1624 minijinja::Value::default(),
1625 ));
1626
1627 let output_dir = config
1629 .php
1630 .as_ref()
1631 .and_then(|p| p.stubs.as_ref())
1632 .map(|s| s.output.to_string_lossy().to_string())
1633 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1634
1635 Ok(vec![GeneratedFile {
1636 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1637 content,
1638 generated_header: false,
1639 }])
1640 }
1641
1642 fn build_config(&self) -> Option<BuildConfig> {
1643 Some(BuildConfig {
1644 tool: "cargo",
1645 crate_suffix: "-php",
1646 build_dep: BuildDependency::None,
1647 post_build: vec![],
1648 })
1649 }
1650}
1651
1652fn bridge_handle_path(api: &ApiSurface, bridge: &alef_core::config::TraitBridgeConfig, core_import: &str) -> String {
1653 let alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
1654 api.types
1655 .iter()
1656 .find(|t| t.name == alias && !t.rust_path.is_empty())
1657 .map(|t| t.rust_path.replace('-', "_"))
1658 .or_else(|| api.excluded_type_paths.get(alias).map(|path| path.replace('-', "_")))
1659 .unwrap_or_else(|| format!("{core_import}::visitor::{alias}"))
1660}
1661
1662fn php_phpdoc_type(ty: &TypeRef) -> String {
1665 match ty {
1666 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1667 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1668 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1669 _ => php_type(ty),
1670 }
1671}
1672
1673fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1675 match ty {
1676 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1677 TypeRef::Map(k, v) => format!(
1678 "array<{}, {}>",
1679 php_phpdoc_type_fq(k, namespace),
1680 php_phpdoc_type_fq(v, namespace)
1681 ),
1682 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1683 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1684 _ => php_type(ty),
1685 }
1686}
1687
1688fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1690 match ty {
1691 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1692 TypeRef::Optional(inner) => {
1693 let inner_type = php_type_fq(inner, namespace);
1694 if inner_type.starts_with('?') {
1695 inner_type
1696 } else {
1697 format!("?{inner_type}")
1698 }
1699 }
1700 _ => php_type(ty),
1701 }
1702}
1703
1704fn gen_php_opaque_class_file(
1711 typ: &alef_core::ir::TypeDef,
1712 namespace: &str,
1713 streaming_adapters: &[&alef_core::config::AdapterConfig],
1714 streaming_method_names: &AHashSet<String>,
1715 trait_bridges: &[alef_core::config::TraitBridgeConfig],
1716) -> String {
1717 let mut content = String::new();
1718 content.push_str(&crate::template_env::render(
1719 "php_file_header.jinja",
1720 minijinja::Value::default(),
1721 ));
1722 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1723 content.push_str(&crate::template_env::render(
1724 "php_declare_strict_types.jinja",
1725 minijinja::Value::default(),
1726 ));
1727 content.push('\n');
1729 content.push_str(&crate::template_env::render(
1730 "php_namespace.jinja",
1731 context! { namespace => namespace },
1732 ));
1733 content.push('\n');
1735
1736 if !typ.doc.is_empty() {
1738 content.push_str("/**\n");
1739 let sanitized = sanitize_rust_idioms(&typ.doc, DocTarget::PhpDoc);
1740 content.push_str(&crate::template_env::render(
1741 "php_phpdoc_lines.jinja",
1742 context! {
1743 doc_lines => sanitized.lines().collect::<Vec<_>>(),
1744 indent => "",
1745 },
1746 ));
1747 content.push_str(" */\n");
1748 }
1749
1750 content.push_str(&format!("final class {}\n{{\n", typ.name));
1751
1752 let mut method_order: Vec<&alef_core::ir::MethodDef> = Vec::new();
1755 method_order.extend(
1756 typ.methods
1757 .iter()
1758 .filter(|m| m.receiver.is_some() && !streaming_method_names.contains(&m.name)),
1759 );
1760 method_order.extend(
1761 typ.methods
1762 .iter()
1763 .filter(|m| m.receiver.is_none() && !streaming_method_names.contains(&m.name)),
1764 );
1765
1766 for method in method_order {
1767 let method_name = method.name.to_lower_camel_case();
1768 let return_type = php_type(&method.return_type);
1769 let is_void = matches!(&method.return_type, TypeRef::Unit);
1770 let is_static = method.receiver.is_none();
1771
1772 let mut doc_lines: Vec<String> = vec![];
1774 let sanitized = sanitize_rust_idioms(&method.doc, DocTarget::PhpDoc);
1775 let doc_line = sanitized.lines().next().unwrap_or("").trim();
1776 if !doc_line.is_empty() {
1777 doc_lines.push(doc_line.to_string());
1778 }
1779
1780 let mut phpdoc_params: Vec<String> = vec![];
1782 for param in &method.params {
1783 if matches!(¶m.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)) {
1784 let phpdoc_type = php_phpdoc_type(¶m.ty);
1785 phpdoc_params.push(format!("@param {} ${}", phpdoc_type, param.name));
1786 }
1787 }
1788 doc_lines.extend(phpdoc_params);
1789
1790 let needs_return_phpdoc = matches!(&method.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _));
1792 if needs_return_phpdoc {
1793 let phpdoc_type = php_phpdoc_type(&method.return_type);
1794 doc_lines.push(format!("@return {phpdoc_type}"));
1795 }
1796
1797 if !doc_lines.is_empty() {
1799 content.push_str(" /**\n");
1800 for line in doc_lines {
1801 content.push_str(&format!(" * {}\n", line));
1802 }
1803 content.push_str(" */\n");
1804 }
1805
1806 let static_kw = if is_static { "static " } else { "" };
1808 let first_optional_idx = method.params.iter().position(|p| p.optional);
1809 let params: Vec<String> = method
1810 .params
1811 .iter()
1812 .enumerate()
1813 .map(|(idx, p)| {
1814 let ptype = php_type(&p.ty);
1815 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1816 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1817 format!("{nullable}{ptype} ${} = null", p.name)
1818 } else {
1819 format!("{} ${}", ptype, p.name)
1820 }
1821 })
1822 .collect();
1823 content.push_str(&format!(
1824 " public {static_kw}function {method_name}({}): {return_type}\n",
1825 params.join(", ")
1826 ));
1827 let body = if is_void {
1828 " {\n }\n"
1829 } else {
1830 " {\n throw new \\RuntimeException('Not implemented — provided by the native extension.');\n }\n"
1831 };
1832 content.push_str(body);
1833 }
1834
1835 for adapter in streaming_adapters {
1837 let item_type = adapter.item_type.as_deref().unwrap_or("array");
1838 content.push_str(&gen_php_streaming_method_wrapper(adapter, item_type));
1839 content.push('\n');
1840 }
1841
1842 for bridge in trait_bridges {
1844 if let Some(ref type_alias) = bridge.type_alias {
1845 if type_alias == &typ.name {
1846 content.push_str(" /**\n");
1848 content
1849 .push_str(" * Wrap a PHP object implementing the visitor interface as a shareable handle.\n");
1850 content.push_str(" */\n");
1851 content.push_str(" public static function from_php_object(object $visitor): self\n");
1852 content.push_str(" {\n");
1853 content.push_str(
1854 " throw new \\RuntimeException('Not implemented — provided by the native extension.');\n",
1855 );
1856 content.push_str(" }\n");
1857 }
1858 }
1859 }
1860
1861 content.push_str("}\n");
1862 content
1863}
1864
1865fn gen_php_streaming_method_wrapper(adapter: &alef_core::config::AdapterConfig, _item_type: &str) -> String {
1871 let method_name = adapter.name.to_lower_camel_case();
1872
1873 let mut params_vec: Vec<String> = Vec::new();
1875
1876 for p in &adapter.params {
1877 let ptype = php_type(&alef_core::ir::TypeRef::Named(p.ty.clone()));
1878 let nullable = if p.optional { "?" } else { "" };
1879 let default = if p.optional { " = null" } else { "" };
1880 params_vec.push(format!("{nullable}{ptype} ${}{default}", p.name));
1881 }
1882
1883 let params_sig = params_vec.join(", ");
1884
1885 format!(
1890 " public function {method_name}({params_sig}): \\Generator\n {{\n \
1891 throw new \\RuntimeException('Not implemented — provided by the native extension.');\n \
1892 }}\n",
1893 method_name = method_name,
1894 )
1895}
1896
1897fn php_type(ty: &TypeRef) -> String {
1899 match ty {
1900 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1901 TypeRef::Primitive(p) => match p {
1902 PrimitiveType::Bool => "bool".to_string(),
1903 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1904 PrimitiveType::U8
1905 | PrimitiveType::U16
1906 | PrimitiveType::U32
1907 | PrimitiveType::U64
1908 | PrimitiveType::I8
1909 | PrimitiveType::I16
1910 | PrimitiveType::I32
1911 | PrimitiveType::I64
1912 | PrimitiveType::Usize
1913 | PrimitiveType::Isize => "int".to_string(),
1914 },
1915 TypeRef::Optional(inner) => {
1916 let inner_type = php_type(inner);
1919 if inner_type.starts_with('?') {
1920 inner_type
1921 } else {
1922 format!("?{inner_type}")
1923 }
1924 }
1925 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1926 TypeRef::Named(name) => name.clone(),
1927 TypeRef::Unit => "void".to_string(),
1928 TypeRef::Duration => "float".to_string(),
1929 }
1930}
1931
1932fn gen_streaming_adapter_facade_method(
1939 adapter: &alef_core::config::AdapterConfig,
1940 _mapper: &crate::type_map::PhpMapper,
1941 _opaque_types: &ahash::AHashSet<String>,
1942 _core_import: &str,
1943) -> String {
1944 use heck::ToLowerCamelCase;
1945
1946 let method_name = adapter.name.to_lower_camel_case();
1947 let owner_type = adapter.owner_type.as_deref().unwrap_or("EngineHandle");
1948
1949 let mut params: Vec<String> = vec![format!("engine: &{owner_type}")];
1951
1952 for p in &adapter.params {
1954 let param_type = p.ty.rsplit("::").next().unwrap_or(&p.ty);
1955 let ref_indicator = if matches!(param_type, "String" | "Vec<String>") {
1956 "" } else {
1958 "&"
1959 };
1960 let nullable = if p.optional { "Option<" } else { "" };
1961 let close_nullable = if p.optional { ">" } else { "" };
1962 params.push(format!(
1963 "{}: {}{}{}{}",
1964 p.name, ref_indicator, nullable, param_type, close_nullable
1965 ));
1966 }
1967
1968 let return_type = "std::result::Result<Vec<String>, ext_php_rs::exception::PhpException>";
1969
1970 let mut method_code = String::new();
1971
1972 method_code.push_str(&format!(
1974 " #[php(name = \"{}\")]\n",
1975 method_name
1976 ));
1977 method_code.push_str(&format!(
1978 " pub fn {}({}) -> {} {{\n",
1979 method_name,
1980 params.join(", "),
1981 return_type
1982 ));
1983
1984 let rust_method_name = &adapter.name;
1987 let call_args = adapter
1988 .params
1989 .iter()
1990 .map(|p| format!("&{}", p.name))
1991 .collect::<Vec<_>>()
1992 .join(", ");
1993
1994 method_code.push_str(&format!(
1995 " engine.{}({})\n",
1996 rust_method_name, call_args
1997 ));
1998
1999 method_code.push_str(" }\n");
2000
2001 method_code
2002}
2003
2004fn php_property_phpdoc(var_type: &str, doc: &str, indent: &str) -> String {
2013 let doc = doc.trim();
2014 if doc.is_empty() {
2015 return format!("{indent}/** @var {var_type} */\n");
2016 }
2017 let lines: Vec<&str> = doc.lines().collect();
2018 if lines.len() == 1 {
2019 let line = lines[0].trim();
2020 return format!("{indent}/** @var {var_type} {line} */\n");
2021 }
2022 let mut out = format!("{indent}/**\n");
2024 for line in &lines {
2025 let trimmed = line.trim();
2026 if trimmed.is_empty() {
2027 out.push_str(&format!("{indent} *\n"));
2028 } else {
2029 out.push_str(&format!("{indent} * {trimmed}\n"));
2030 }
2031 }
2032 out.push_str(&format!("{indent} *\n"));
2033 out.push_str(&format!("{indent} * @var {var_type}\n"));
2034 out.push_str(&format!("{indent} */\n"));
2035 out
2036}