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 bridge_cfg in &config.trait_bridges {
428 if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
429 method_items.push(format!(
430 "pub fn {}(backend: &mut ext_php_rs::types::ZendObject) -> ext_php_rs::prelude::PhpResult<()> {{\n \
431 crate::{}(backend)\n}}",
432 register_fn,
433 register_fn
434 ));
435 }
436 if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
437 method_items.push(format!(
438 "pub fn {}(name: String) -> ext_php_rs::prelude::PhpResult<()> {{\n \
439 crate::{}(name)\n}}",
440 unregister_fn, unregister_fn
441 ));
442 }
443 if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
444 method_items.push(format!(
445 "pub fn {}() -> ext_php_rs::prelude::PhpResult<()> {{\n \
446 crate::{}()\n}}",
447 clear_fn, clear_fn
448 ));
449 }
450 }
451
452 let methods_joined = method_items
453 .iter()
454 .map(|m| {
455 m.lines()
457 .map(|l| {
458 if l.is_empty() {
459 String::new()
460 } else {
461 format!(" {l}")
462 }
463 })
464 .collect::<Vec<_>>()
465 .join("\n")
466 })
467 .collect::<Vec<_>>()
468 .join("\n\n");
469 let php_api_class_name = format!("{facade_class_name}Api");
472 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
474 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
475 let facade_struct = format!(
476 "#[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}}"
477 );
478 builder.add_item(&facade_struct);
479
480 for bridge_cfg in &config.trait_bridges {
482 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
483 let bridge = crate::trait_bridge::gen_trait_bridge(
484 trait_type,
485 bridge_cfg,
486 &core_import,
487 &config.error_type_name(),
488 &config.error_constructor_expr(),
489 api,
490 );
491 for imp in &bridge.imports {
492 builder.add_import(imp);
493 }
494 builder.add_item(&bridge.code);
495 }
496 }
497 }
498
499 let convertible = alef_codegen::conversions::convertible_types(api);
500 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
501 let input_types = alef_codegen::conversions::input_type_names(api);
502 let enum_names_ref = &mapper.enum_names;
507 let bridge_skip_types: Vec<String> = config
508 .trait_bridges
509 .iter()
510 .filter(|b| !matches!(b.bind_via, alef_core::config::BridgeBinding::OptionsField))
511 .filter_map(|b| b.type_alias.clone())
512 .collect();
513 let trait_bridge_arc_wrapper_field_names: Vec<String> = config
518 .trait_bridges
519 .iter()
520 .filter(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField)
521 .filter_map(|b| b.resolved_options_field().map(String::from))
522 .collect();
523 let mut conv_opaque_types: AHashSet<String> = opaque_types.clone();
528 for bridge in &config.trait_bridges {
529 if let Some(alias) = &bridge.type_alias {
530 conv_opaque_types.insert(alias.clone());
531 }
532 }
533 let php_conv_config = ConversionConfig {
534 cast_large_ints_to_i64: true,
535 enum_string_names: Some(enum_names_ref),
536 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
537 json_as_value: true,
541 include_cfg_metadata: false,
542 option_duration_on_defaults: true,
543 from_binding_skip_types: &bridge_skip_types,
544 never_skip_cfg_field_names: &never_skip_cfg_field_names,
545 opaque_types: Some(&conv_opaque_types),
546 trait_bridge_arc_wrapper_field_names: &trait_bridge_arc_wrapper_field_names,
547 ..Default::default()
548 };
549 let mut enum_tainted: AHashSet<String> = AHashSet::new();
551 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
552 if has_enum_named_field(typ, enum_names_ref) {
553 enum_tainted.insert(typ.name.clone());
554 }
555 }
556 let mut changed = true;
558 while changed {
559 changed = false;
560 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
561 if !enum_tainted.contains(&typ.name)
562 && binding_fields(&typ.fields).any(|f| references_named_type(&f.ty, &enum_tainted))
563 {
564 enum_tainted.insert(typ.name.clone());
565 changed = true;
566 }
567 }
568 }
569 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
570 if input_types.contains(&typ.name)
572 && !enum_tainted.contains(&typ.name)
573 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
574 {
575 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
576 typ,
577 &core_import,
578 &php_conv_config,
579 ));
580 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
581 builder.add_item(&gen_enum_tainted_from_binding_to_core(
588 typ,
589 &core_import,
590 enum_names_ref,
591 &enum_tainted,
592 &php_conv_config,
593 &api.enums,
594 &bridge_type_aliases_set,
595 ));
596 }
597 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
599 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
600 typ,
601 &core_import,
602 &opaque_types,
603 &php_conv_config,
604 ));
605 }
606 }
607
608 let mut emitted_binding_to_core: AHashSet<String> = api
618 .types
619 .iter()
620 .filter(|typ| !typ.is_trait && input_types.contains(&typ.name))
621 .filter(|typ| {
622 (enum_tainted.contains(&typ.name))
623 || alef_codegen::conversions::can_generate_conversion(typ, &convertible)
624 })
625 .map(|typ| typ.name.clone())
626 .collect();
627 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
628 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
629 for variant in &enum_def.variants {
632 for field in &variant.fields {
633 if let TypeRef::Named(type_name) = &field.ty {
634 if let Some(typ) = api.types.iter().find(|t| &t.name == type_name) {
635 if emitted_binding_to_core.contains(&typ.name) {
636 continue;
637 }
638 if enum_tainted.contains(&typ.name) {
639 builder.add_item(&gen_enum_tainted_from_binding_to_core(
640 typ,
641 &core_import,
642 enum_names_ref,
643 &enum_tainted,
644 &php_conv_config,
645 &api.enums,
646 &bridge_type_aliases_set,
647 ));
648 emitted_binding_to_core.insert(typ.name.clone());
649 } else if alef_codegen::conversions::can_generate_conversion(typ, &convertible) {
650 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
651 typ,
652 &core_import,
653 &php_conv_config,
654 ));
655 emitted_binding_to_core.insert(typ.name.clone());
656 }
657 }
658 }
659 }
660 }
661 }
662
663 for typ in api.types.iter().filter(|t| !t.is_trait) {
667 if !emitted_binding_to_core.contains(&typ.name) {
668 if enum_tainted.contains(&typ.name) {
669 builder.add_item(&gen_enum_tainted_from_binding_to_core(
670 typ,
671 &core_import,
672 enum_names_ref,
673 &enum_tainted,
674 &php_conv_config,
675 &api.enums,
676 &bridge_type_aliases_set,
677 ));
678 emitted_binding_to_core.insert(typ.name.clone());
679 } else if alef_codegen::conversions::can_generate_conversion(typ, &convertible) {
680 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
681 typ,
682 &core_import,
683 &php_conv_config,
684 ));
685 emitted_binding_to_core.insert(typ.name.clone());
686 }
687 }
688 }
689
690 for error in &api.errors {
692 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
693 let methods_impl = alef_codegen::error_gen::gen_php_error_methods_impl(error, &core_import);
695 if !methods_impl.is_empty() {
696 builder.add_item(&methods_impl);
697 }
698 }
699
700 if has_serde {
704 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
705 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
706 pub fn max_compression_ratio() -> i64 { 100 }\n\
707 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
708 pub fn max_nesting_depth() -> i64 { 1024 }\n\
709 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
710 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
711 pub fn max_iterations() -> i64 { 10_000_000 }\n\
712 pub fn max_xml_depth() -> i64 { 1024 }\n\
713 pub fn max_table_cells() -> i64 { 100_000 }\n\
714 }";
715 builder.add_item(serde_module);
716 }
717
718 let php_config = config.php.as_ref();
724 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
725
726 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
730 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
731 }
732
733 let mut class_registrations = String::new();
736 for typ in api
737 .types
738 .iter()
739 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
740 {
741 class_registrations.push_str(&crate::template_env::render(
742 "php_class_registration.jinja",
743 context! { class_name => &typ.name },
744 ));
745 }
746 if !api.functions.is_empty() {
748 let facade_class_name = extension_name.to_pascal_case();
749 class_registrations.push_str(&crate::template_env::render(
750 "php_class_registration.jinja",
751 context! { class_name => &format!("{facade_class_name}Api") },
752 ));
753 }
754 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
757 class_registrations.push_str(&crate::template_env::render(
758 "php_class_registration.jinja",
759 context! { class_name => &enum_def.name },
760 ));
761 }
762 for error in api.errors.iter().filter(|e| !e.methods.is_empty()) {
764 let info_class = format!("{}Info", error.name);
765 class_registrations.push_str(&crate::template_env::render(
766 "php_class_registration.jinja",
767 context! { class_name => &info_class },
768 ));
769 }
770 builder.add_item(&format!(
771 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
772 ));
773
774 let mut content = builder.build();
775
776 for bridge in &config.trait_bridges {
781 if let Some(field_name) = bridge.resolved_options_field() {
782 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
783 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
784 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
785 let builder_type = format!("{}Builder", options_type);
786 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
787 let bridge_handle_path = bridge_handle_path(api, bridge, &core_import);
788
789 let old_method = format!(
795 " 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 }}"
796 );
797 let new_method = format!(
798 " 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 }}"
799 );
800
801 content = content.replace(&old_method, &new_method);
802 }
803 }
804
805 let php_stubs_dir = config
808 .php
809 .as_ref()
810 .and_then(|p| p.stubs.as_ref())
811 .map(|s| s.output.to_string_lossy().to_string())
812 .unwrap_or_else(|| "packages/php/src/".to_string());
813
814 let php_namespace = php_autoload_namespace(config);
815
816 let mut generated_files = vec![GeneratedFile {
817 path: PathBuf::from(&output_dir).join("lib.rs"),
818 content,
819 generated_header: false,
820 }];
821
822 for bridge_cfg in &config.trait_bridges {
824 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
825 let is_visitor_bridge = bridge_cfg.type_alias.is_some()
827 && bridge_cfg.register_fn.is_none()
828 && bridge_cfg.super_trait.is_none()
829 && trait_type.methods.iter().all(|m| m.has_default_impl);
830
831 if is_visitor_bridge {
832 let interface_content = crate::trait_bridge::gen_visitor_interface(
833 trait_type,
834 bridge_cfg,
835 &php_namespace,
836 &HashMap::new(), );
838 let interface_filename = format!("{}Interface.php", bridge_cfg.trait_name);
839 generated_files.push(GeneratedFile {
840 path: PathBuf::from(&php_stubs_dir).join(&interface_filename),
841 content: interface_content,
842 generated_header: false,
843 });
844 }
845 }
846 }
847
848 Ok(generated_files)
849 }
850
851 fn generate_public_api(
852 &self,
853 api: &ApiSurface,
854 config: &ResolvedCrateConfig,
855 ) -> anyhow::Result<Vec<GeneratedFile>> {
856 let escape_phpdoc_line = |s: &str| s.replace("*/", "* /");
858
859 let extension_name = config.php_extension_name();
860 let class_name = extension_name.to_pascal_case();
861
862 let mut content = String::new();
864 content.push_str(&crate::template_env::render(
865 "php_file_header.jinja",
866 minijinja::Value::default(),
867 ));
868 content.push_str(&hash::header(CommentStyle::DoubleSlash));
869 content.push_str(&crate::template_env::render(
870 "php_declare_strict_types.jinja",
871 minijinja::Value::default(),
872 ));
873 content.push('\n');
875
876 let namespace = php_autoload_namespace(config);
878
879 content.push_str(&crate::template_env::render(
880 "php_namespace.jinja",
881 context! { namespace => &namespace },
882 ));
883 content.push('\n');
885 content.push_str(&crate::template_env::render(
886 "php_facade_class_declaration.jinja",
887 context! { class_name => &class_name },
888 ));
889
890 let bridge_param_names_pub: ahash::AHashSet<&str> = config
892 .trait_bridges
893 .iter()
894 .filter_map(|b| b.param_name.as_deref())
895 .collect();
896
897 let no_arg_constructor_types: AHashSet<String> = api
902 .types
903 .iter()
904 .filter(|t| t.fields.iter().all(|f| f.optional))
905 .map(|t| t.name.clone())
906 .collect();
907
908 for func in &api.functions {
910 if alef_codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&func.name, &config.trait_bridges) {
914 continue;
915 }
916 let method_name = func.name.to_lower_camel_case();
921 let return_php_type = php_type(&func.return_type);
922
923 let visible_params: Vec<_> = func
925 .params
926 .iter()
927 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
928 .collect();
929
930 content.push_str(&crate::template_env::render(
932 "php_phpdoc_block_start.jinja",
933 minijinja::Value::default(),
934 ));
935 if func.doc.is_empty() {
936 content.push_str(&crate::template_env::render(
937 "php_phpdoc_text_line.jinja",
938 context! { text => &format!("{}.", method_name) },
939 ));
940 } else {
941 let sections = doc_emission::parse_rustdoc_sections(&func.doc);
943 for line in sections.summary.lines() {
945 content.push_str(" * ");
946 content.push_str(&escape_phpdoc_line(line));
947 content.push('\n');
948 }
949 }
952 content.push_str(&crate::template_env::render(
953 "php_phpdoc_empty_line.jinja",
954 minijinja::Value::default(),
955 ));
956 for p in &visible_params {
957 let ptype = php_phpdoc_type(&p.ty);
958 let nullable_prefix = if p.optional { "?" } else { "" };
959 content.push_str(&crate::template_env::render(
960 "php_phpdoc_param_line.jinja",
961 context! {
962 nullable_prefix => nullable_prefix,
963 param_type => &ptype,
964 param_name => &p.name,
965 },
966 ));
967 }
968 let return_phpdoc = php_phpdoc_type(&func.return_type);
969 content.push_str(&crate::template_env::render(
970 "php_phpdoc_return_line.jinja",
971 context! { return_type => &return_phpdoc },
972 ));
973 if func.error_type.is_some() {
974 content.push_str(&crate::template_env::render(
975 "php_phpdoc_throws_line.jinja",
976 context! {
977 namespace => namespace.as_str(),
978 class_name => &class_name,
979 },
980 ));
981 }
982 content.push_str(&crate::template_env::render(
983 "php_phpdoc_block_end.jinja",
984 minijinja::Value::default(),
985 ));
986
987 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
998 if let TypeRef::Named(name) = &p.ty {
999 (name.ends_with("Config") || name.as_str() == "config")
1000 && no_arg_constructor_types.contains(name.as_str())
1001 } else {
1002 false
1003 }
1004 };
1005
1006 let mut first_optional_idx = None;
1007 for (idx, p) in visible_params.iter().enumerate() {
1008 if p.optional || is_optional_config_param(p) {
1009 first_optional_idx = Some(idx);
1010 break;
1011 }
1012 }
1013
1014 content.push_str(&crate::template_env::render(
1015 "php_method_signature_start.jinja",
1016 context! { method_name => &method_name },
1017 ));
1018
1019 let params: Vec<String> = visible_params
1020 .iter()
1021 .enumerate()
1022 .map(|(idx, p)| {
1023 let ptype = php_type(&p.ty);
1024 let should_be_optional = p.optional
1029 || is_optional_config_param(p)
1030 || first_optional_idx.is_some_and(|first| idx >= first);
1031 if should_be_optional {
1032 format!("?{} ${} = null", ptype, p.name)
1033 } else {
1034 format!("{} ${}", ptype, p.name)
1035 }
1036 })
1037 .collect();
1038 content.push_str(¶ms.join(", "));
1039 content.push_str(&crate::template_env::render(
1040 "php_method_signature_end.jinja",
1041 context! { return_type => &return_php_type },
1042 ));
1043 let ext_method_name = func.name.to_lower_camel_case();
1048 let is_void = matches!(&func.return_type, TypeRef::Unit);
1049 let call_params = visible_params
1057 .iter()
1058 .enumerate()
1059 .map(|(idx, p)| {
1060 let should_be_optional = p.optional
1061 || is_optional_config_param(p)
1062 || first_optional_idx.is_some_and(|first| idx >= first);
1063 if should_be_optional && is_optional_config_param(p) {
1064 if let TypeRef::Named(type_name) = &p.ty {
1065 return format!("${} ?? new {}()", p.name, type_name);
1066 }
1067 }
1068 format!("${}", p.name)
1069 })
1070 .collect::<Vec<_>>()
1071 .join(", ");
1072 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
1073 if is_void {
1074 content.push_str(&crate::template_env::render(
1075 "php_method_call_statement.jinja",
1076 context! { call_expr => &call_expr },
1077 ));
1078 } else {
1079 content.push_str(&crate::template_env::render(
1080 "php_method_call_return.jinja",
1081 context! { call_expr => &call_expr },
1082 ));
1083 }
1084 content.push_str(&crate::template_env::render(
1085 "php_method_end.jinja",
1086 minijinja::Value::default(),
1087 ));
1088 }
1089
1090 for bridge_cfg in &config.trait_bridges {
1092 if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
1093 let method_name = register_fn.to_lower_camel_case();
1094 content.push_str(&crate::template_env::render(
1095 "php_phpdoc_block_start.jinja",
1096 minijinja::Value::default(),
1097 ));
1098 content.push_str(&crate::template_env::render(
1099 "php_phpdoc_text_line.jinja",
1100 context! { text => &format!("{}.", method_name) },
1101 ));
1102 content.push_str(&crate::template_env::render(
1103 "php_phpdoc_empty_line.jinja",
1104 minijinja::Value::default(),
1105 ));
1106 let interface_name = &bridge_cfg.trait_name;
1107 content.push_str(&crate::template_env::render(
1108 "php_phpdoc_param_line.jinja",
1109 context! {
1110 nullable_prefix => "",
1111 param_type => interface_name,
1112 param_name => "backend",
1113 },
1114 ));
1115 content.push_str(&crate::template_env::render(
1116 "php_phpdoc_return_line.jinja",
1117 context! { return_type => "void" },
1118 ));
1119 content.push_str(&crate::template_env::render(
1120 "php_phpdoc_block_end.jinja",
1121 minijinja::Value::default(),
1122 ));
1123 content.push_str(&crate::template_env::render(
1124 "php_method_signature_start.jinja",
1125 context! { method_name => &method_name },
1126 ));
1127 content.push_str(&format!("{} $backend = null) : void\n {{\n", interface_name));
1128 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}($backend)");
1129 content.push_str(&crate::template_env::render(
1130 "php_method_call_statement.jinja",
1131 context! { call_expr => &call_expr },
1132 ));
1133 content.push_str(&crate::template_env::render(
1134 "php_method_end.jinja",
1135 minijinja::Value::default(),
1136 ));
1137 }
1138 if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
1139 let method_name = unregister_fn.to_lower_camel_case();
1140 content.push_str(&crate::template_env::render(
1141 "php_phpdoc_block_start.jinja",
1142 minijinja::Value::default(),
1143 ));
1144 content.push_str(&crate::template_env::render(
1145 "php_phpdoc_text_line.jinja",
1146 context! { text => &format!("{}.", method_name) },
1147 ));
1148 content.push_str(&crate::template_env::render(
1149 "php_phpdoc_empty_line.jinja",
1150 minijinja::Value::default(),
1151 ));
1152 content.push_str(&crate::template_env::render(
1153 "php_phpdoc_param_line.jinja",
1154 context! {
1155 nullable_prefix => "",
1156 param_type => "string",
1157 param_name => "name",
1158 },
1159 ));
1160 content.push_str(&crate::template_env::render(
1161 "php_phpdoc_return_line.jinja",
1162 context! { return_type => "void" },
1163 ));
1164 content.push_str(&crate::template_env::render(
1165 "php_phpdoc_block_end.jinja",
1166 minijinja::Value::default(),
1167 ));
1168 content.push_str(&crate::template_env::render(
1169 "php_method_signature_start.jinja",
1170 context! { method_name => &method_name },
1171 ));
1172 content.push_str("string $name) : void\n {\n");
1173 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}($name)");
1174 content.push_str(&crate::template_env::render(
1175 "php_method_call_statement.jinja",
1176 context! { call_expr => &call_expr },
1177 ));
1178 content.push_str(&crate::template_env::render(
1179 "php_method_end.jinja",
1180 minijinja::Value::default(),
1181 ));
1182 }
1183 if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
1184 let method_name = clear_fn.to_lower_camel_case();
1185 content.push_str(&crate::template_env::render(
1186 "php_phpdoc_block_start.jinja",
1187 minijinja::Value::default(),
1188 ));
1189 content.push_str(&crate::template_env::render(
1190 "php_phpdoc_text_line.jinja",
1191 context! { text => &format!("{}.", method_name) },
1192 ));
1193 content.push_str(&crate::template_env::render(
1194 "php_phpdoc_empty_line.jinja",
1195 minijinja::Value::default(),
1196 ));
1197 content.push_str(&crate::template_env::render(
1198 "php_phpdoc_return_line.jinja",
1199 context! { return_type => "void" },
1200 ));
1201 content.push_str(&crate::template_env::render(
1202 "php_phpdoc_block_end.jinja",
1203 minijinja::Value::default(),
1204 ));
1205 content.push_str(&crate::template_env::render(
1206 "php_method_signature_start.jinja",
1207 context! { method_name => &method_name },
1208 ));
1209 content.push_str(") : void\n {\n");
1210 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}()");
1211 content.push_str(&crate::template_env::render(
1212 "php_method_call_statement.jinja",
1213 context! { call_expr => &call_expr },
1214 ));
1215 content.push_str(&crate::template_env::render(
1216 "php_method_end.jinja",
1217 minijinja::Value::default(),
1218 ));
1219 }
1220 }
1221
1222 content.push_str(&crate::template_env::render(
1223 "php_class_end.jinja",
1224 minijinja::Value::default(),
1225 ));
1226
1227 let output_dir = config
1231 .php
1232 .as_ref()
1233 .and_then(|p| p.stubs.as_ref())
1234 .map(|s| s.output.to_string_lossy().to_string())
1235 .unwrap_or_else(|| "packages/php/src/".to_string());
1236
1237 let mut files: Vec<GeneratedFile> = Vec::new();
1238 files.push(GeneratedFile {
1239 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
1240 content,
1241 generated_header: false,
1242 });
1243
1244 for typ in api.types.iter().filter(|t| t.is_opaque && !t.is_trait) {
1250 let streaming_adapters: Vec<&alef_core::config::AdapterConfig> = config
1251 .adapters
1252 .iter()
1253 .filter(|a| {
1254 matches!(a.pattern, alef_core::config::AdapterPattern::Streaming)
1255 && a.owner_type.as_deref() == Some(&typ.name)
1256 && !a.skip_languages.iter().any(|l| l == "php")
1257 })
1258 .collect();
1259 let streaming_method_names: AHashSet<String> = streaming_adapters.iter().map(|a| a.name.clone()).collect();
1260 let opaque_file = gen_php_opaque_class_file(
1261 typ,
1262 &namespace,
1263 &streaming_adapters,
1264 &streaming_method_names,
1265 &config.trait_bridges,
1266 );
1267 files.push(GeneratedFile {
1268 path: PathBuf::from(&output_dir).join(format!("{}.php", typ.name)),
1269 content: opaque_file,
1270 generated_header: false,
1271 });
1272 }
1273
1274 Ok(files)
1275 }
1276
1277 fn generate_type_stubs(
1278 &self,
1279 api: &ApiSurface,
1280 config: &ResolvedCrateConfig,
1281 ) -> anyhow::Result<Vec<GeneratedFile>> {
1282 let extension_name = config.php_extension_name();
1283 let class_name = extension_name.to_pascal_case();
1284
1285 let namespace = php_autoload_namespace(config);
1287
1288 let mut content = String::new();
1293 content.push_str(&crate::template_env::render(
1294 "php_file_header.jinja",
1295 minijinja::Value::default(),
1296 ));
1297 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1298 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
1299 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
1300 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
1301 content.push_str(&crate::template_env::render(
1302 "php_declare_strict_types.jinja",
1303 minijinja::Value::default(),
1304 ));
1305 content.push('\n');
1307 content.push_str(&crate::template_env::render(
1309 "php_namespace_block_begin.jinja",
1310 context! { namespace => &namespace },
1311 ));
1312
1313 content.push_str(&crate::template_env::render(
1315 "php_exception_class_declaration.jinja",
1316 context! { class_name => &class_name },
1317 ));
1318 content.push_str(
1319 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1320 );
1321 let has_status_code = api
1324 .errors
1325 .iter()
1326 .any(|e| e.methods.iter().any(|m| m.name == "status_code"));
1327 let has_is_transient = api
1328 .errors
1329 .iter()
1330 .any(|e| e.methods.iter().any(|m| m.name == "is_transient"));
1331 let has_error_type = api
1332 .errors
1333 .iter()
1334 .any(|e| e.methods.iter().any(|m| m.name == "error_type"));
1335 if has_status_code {
1336 content.push_str(
1337 " /** HTTP status code for this error (0 means no associated status). */\n \
1338 public function statusCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1339 );
1340 }
1341 if has_is_transient {
1342 content.push_str(
1343 " /** Returns true if the error is transient and a retry may succeed. */\n \
1344 public function isTransient(): bool { throw new \\RuntimeException('Not implemented.'); }\n",
1345 );
1346 }
1347 if has_error_type {
1348 content.push_str(
1349 " /** Machine-readable error category string for matching and logging. */\n \
1350 public function errorType(): string { throw new \\RuntimeException('Not implemented.'); }\n",
1351 );
1352 }
1353 content.push_str("}\n\n");
1354
1355 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
1362 if typ.is_opaque || typ.fields.is_empty() {
1363 continue;
1364 }
1365 if !typ.doc.is_empty() {
1366 content.push_str("/**\n");
1367 let sanitized = sanitize_rust_idioms(&typ.doc, DocTarget::PhpDoc);
1368 content.push_str(&crate::template_env::render(
1369 "php_phpdoc_lines.jinja",
1370 context! {
1371 doc_lines => sanitized.lines().collect::<Vec<_>>(),
1372 indent => "",
1373 },
1374 ));
1375 content.push_str(" */\n");
1376 }
1377 content.push_str(&crate::template_env::render(
1378 "php_record_class_stub_declaration.jinja",
1379 context! { class_name => &typ.name },
1380 ));
1381
1382 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = binding_fields(&typ.fields).collect();
1385 sorted_fields.sort_by_key(|f| f.optional);
1386
1387 let params: Vec<String> = sorted_fields
1392 .iter()
1393 .map(|f| {
1394 let ptype = php_type(&f.ty);
1395 let nullable = if f.optional && !ptype.starts_with('?') {
1396 format!("?{ptype}")
1397 } else {
1398 ptype
1399 };
1400 let default = if f.optional { " = null" } else { "" };
1401 let php_name = to_php_name(&f.name);
1402 let phpdoc_type = php_phpdoc_type(&f.ty);
1403 let var_type = if f.optional && !phpdoc_type.starts_with('?') {
1404 format!("?{phpdoc_type}")
1405 } else {
1406 phpdoc_type
1407 };
1408 let phpdoc = php_property_phpdoc(&var_type, &f.doc, " ");
1409 format!("{phpdoc} public readonly {nullable} ${php_name}{default}",)
1410 })
1411 .collect();
1412 content.push_str(&crate::template_env::render(
1413 "php_constructor_method.jinja",
1414 context! { params => ¶ms.join(",\n") },
1415 ));
1416
1417 let non_excluded_methods: Vec<&alef_core::ir::MethodDef> = typ
1422 .methods
1423 .iter()
1424 .filter(|m| !m.binding_excluded && !m.sanitized)
1425 .collect();
1426 for method in non_excluded_methods {
1427 let method_name = method.name.to_lower_camel_case();
1428 let is_static = method.receiver.is_none();
1429 let return_type = php_type(&method.return_type);
1430 let first_optional_idx = method.params.iter().position(|p| p.optional);
1431 let params: Vec<String> = method
1432 .params
1433 .iter()
1434 .enumerate()
1435 .map(|(idx, p)| {
1436 let ptype = php_type(&p.ty);
1437 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1438 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1439 format!("{nullable}{ptype} ${} = null", p.name)
1440 } else {
1441 format!("{} ${}", ptype, p.name)
1442 }
1443 })
1444 .collect();
1445 let static_kw = if is_static { "static " } else { "" };
1446 let is_void = matches!(&method.return_type, TypeRef::Unit);
1447 let stub_body = if is_void {
1448 "{ }".to_string()
1449 } else {
1450 "{ throw new \\RuntimeException('Not implemented — provided by the native extension.'); }"
1451 .to_string()
1452 };
1453 content.push_str(&format!(
1454 " public {static_kw}function {method_name}({}): {return_type}\n {stub_body}\n",
1455 params.join(", ")
1456 ));
1457 }
1458
1459 content.push_str("}\n\n");
1460 }
1461
1462 for enum_def in &api.enums {
1465 if is_tagged_data_enum(enum_def) {
1466 if !enum_def.doc.is_empty() {
1468 content.push_str("/**\n");
1469 let sanitized = sanitize_rust_idioms(&enum_def.doc, DocTarget::PhpDoc);
1470 content.push_str(&crate::template_env::render(
1471 "php_phpdoc_lines.jinja",
1472 context! {
1473 doc_lines => sanitized.lines().collect::<Vec<_>>(),
1474 indent => "",
1475 },
1476 ));
1477 content.push_str(" */\n");
1478 }
1479 content.push_str(&crate::template_env::render(
1480 "php_record_class_stub_declaration.jinja",
1481 context! { class_name => &enum_def.name },
1482 ));
1483 content.push_str("}\n\n");
1484 } else {
1485 content.push_str(&crate::template_env::render(
1487 "php_tagged_enum_declaration.jinja",
1488 context! { enum_name => &enum_def.name },
1489 ));
1490 for variant in &enum_def.variants {
1491 let case_name = sanitize_php_enum_case(&variant.name);
1492 content.push_str(&crate::template_env::render(
1493 "php_enum_variant_stub.jinja",
1494 context! {
1495 variant_name => case_name,
1496 value => &variant.name,
1497 },
1498 ));
1499 }
1500 content.push_str("}\n\n");
1501 }
1502 }
1503
1504 if !api.functions.is_empty() {
1509 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1511 .trait_bridges
1512 .iter()
1513 .filter_map(|b| b.param_name.as_deref())
1514 .collect();
1515
1516 content.push_str(&crate::template_env::render(
1517 "php_api_class_declaration.jinja",
1518 context! { class_name => &class_name },
1519 ));
1520 for func in &api.functions {
1521 let return_type = php_type_fq(&func.return_type, &namespace);
1522 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1523 let visible_params: Vec<_> = func
1525 .params
1526 .iter()
1527 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1528 .collect();
1529 let has_array_params = visible_params
1536 .iter()
1537 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1538 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1539 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1540 let first_optional_idx = visible_params.iter().position(|p| p.optional);
1541 if has_array_params || has_array_return {
1542 content.push_str(" /**\n");
1543 for (idx, p) in visible_params.iter().enumerate() {
1544 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1545 let nullable_prefix = if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1546 "?"
1547 } else {
1548 ""
1549 };
1550 content.push_str(&crate::template_env::render(
1551 "php_phpdoc_static_param.jinja",
1552 context! {
1553 nullable_prefix => nullable_prefix,
1554 ptype => &ptype,
1555 param_name => &p.name,
1556 },
1557 ));
1558 }
1559 content.push_str(&crate::template_env::render(
1560 "php_phpdoc_static_return.jinja",
1561 context! { return_phpdoc => &return_phpdoc },
1562 ));
1563 content.push_str(" */\n");
1564 }
1565 let params: Vec<String> = visible_params
1566 .iter()
1567 .enumerate()
1568 .map(|(idx, p)| {
1569 let ptype = php_type_fq(&p.ty, &namespace);
1570 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1571 let nullable_ptype = if ptype.starts_with('?') {
1572 ptype
1573 } else {
1574 format!("?{ptype}")
1575 };
1576 format!("{} ${} = null", nullable_ptype, p.name)
1577 } else {
1578 format!("{} ${}", ptype, p.name)
1579 }
1580 })
1581 .collect();
1582 let stub_method_name = func.name.to_lower_camel_case();
1586 let is_void_stub = return_type == "void";
1587 let stub_body = if is_void_stub {
1588 "{ }".to_string()
1589 } else {
1590 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1591 };
1592 content.push_str(&crate::template_env::render(
1593 "php_static_method_stub.jinja",
1594 context! {
1595 method_name => &stub_method_name,
1596 params => ¶ms.join(", "),
1597 return_type => &return_type,
1598 stub_body => &stub_body,
1599 },
1600 ));
1601 }
1602 content.push_str("}\n\n");
1603 }
1604
1605 content.push_str(&crate::template_env::render(
1607 "php_namespace_block_end.jinja",
1608 minijinja::Value::default(),
1609 ));
1610
1611 let output_dir = config
1613 .php
1614 .as_ref()
1615 .and_then(|p| p.stubs.as_ref())
1616 .map(|s| s.output.to_string_lossy().to_string())
1617 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1618
1619 Ok(vec![GeneratedFile {
1620 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1621 content,
1622 generated_header: false,
1623 }])
1624 }
1625
1626 fn build_config(&self) -> Option<BuildConfig> {
1627 Some(BuildConfig {
1628 tool: "cargo",
1629 crate_suffix: "-php",
1630 build_dep: BuildDependency::None,
1631 post_build: vec![],
1632 })
1633 }
1634}
1635
1636fn bridge_handle_path(api: &ApiSurface, bridge: &alef_core::config::TraitBridgeConfig, core_import: &str) -> String {
1637 let alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
1638 api.types
1639 .iter()
1640 .find(|t| t.name == alias && !t.rust_path.is_empty())
1641 .map(|t| t.rust_path.replace('-', "_"))
1642 .or_else(|| api.excluded_type_paths.get(alias).map(|path| path.replace('-', "_")))
1643 .unwrap_or_else(|| format!("{core_import}::visitor::{alias}"))
1644}
1645
1646fn php_phpdoc_type(ty: &TypeRef) -> String {
1649 match ty {
1650 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1651 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1652 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1653 _ => php_type(ty),
1654 }
1655}
1656
1657fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1659 match ty {
1660 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1661 TypeRef::Map(k, v) => format!(
1662 "array<{}, {}>",
1663 php_phpdoc_type_fq(k, namespace),
1664 php_phpdoc_type_fq(v, namespace)
1665 ),
1666 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1667 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1668 _ => php_type(ty),
1669 }
1670}
1671
1672fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1674 match ty {
1675 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1676 TypeRef::Optional(inner) => {
1677 let inner_type = php_type_fq(inner, namespace);
1678 if inner_type.starts_with('?') {
1679 inner_type
1680 } else {
1681 format!("?{inner_type}")
1682 }
1683 }
1684 _ => php_type(ty),
1685 }
1686}
1687
1688fn gen_php_opaque_class_file(
1695 typ: &alef_core::ir::TypeDef,
1696 namespace: &str,
1697 streaming_adapters: &[&alef_core::config::AdapterConfig],
1698 streaming_method_names: &AHashSet<String>,
1699 trait_bridges: &[alef_core::config::TraitBridgeConfig],
1700) -> String {
1701 let mut content = String::new();
1702 content.push_str(&crate::template_env::render(
1703 "php_file_header.jinja",
1704 minijinja::Value::default(),
1705 ));
1706 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1707 content.push_str(&crate::template_env::render(
1708 "php_declare_strict_types.jinja",
1709 minijinja::Value::default(),
1710 ));
1711 content.push('\n');
1713 content.push_str(&crate::template_env::render(
1714 "php_namespace.jinja",
1715 context! { namespace => namespace },
1716 ));
1717 content.push('\n');
1719
1720 if !typ.doc.is_empty() {
1722 content.push_str("/**\n");
1723 let sanitized = sanitize_rust_idioms(&typ.doc, DocTarget::PhpDoc);
1724 content.push_str(&crate::template_env::render(
1725 "php_phpdoc_lines.jinja",
1726 context! {
1727 doc_lines => sanitized.lines().collect::<Vec<_>>(),
1728 indent => "",
1729 },
1730 ));
1731 content.push_str(" */\n");
1732 }
1733
1734 content.push_str(&format!("final class {}\n{{\n", typ.name));
1735
1736 let mut method_order: Vec<&alef_core::ir::MethodDef> = Vec::new();
1739 method_order.extend(
1740 typ.methods
1741 .iter()
1742 .filter(|m| m.receiver.is_some() && !streaming_method_names.contains(&m.name)),
1743 );
1744 method_order.extend(
1745 typ.methods
1746 .iter()
1747 .filter(|m| m.receiver.is_none() && !streaming_method_names.contains(&m.name)),
1748 );
1749
1750 for method in method_order {
1751 let method_name = method.name.to_lower_camel_case();
1752 let return_type = php_type(&method.return_type);
1753 let is_void = matches!(&method.return_type, TypeRef::Unit);
1754 let is_static = method.receiver.is_none();
1755
1756 let mut doc_lines: Vec<String> = vec![];
1758 let sanitized = sanitize_rust_idioms(&method.doc, DocTarget::PhpDoc);
1759 let doc_line = sanitized.lines().next().unwrap_or("").trim();
1760 if !doc_line.is_empty() {
1761 doc_lines.push(doc_line.to_string());
1762 }
1763
1764 let mut phpdoc_params: Vec<String> = vec![];
1766 for param in &method.params {
1767 if matches!(¶m.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)) {
1768 let phpdoc_type = php_phpdoc_type(¶m.ty);
1769 phpdoc_params.push(format!("@param {} ${}", phpdoc_type, param.name));
1770 }
1771 }
1772 doc_lines.extend(phpdoc_params);
1773
1774 let needs_return_phpdoc = matches!(&method.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _));
1776 if needs_return_phpdoc {
1777 let phpdoc_type = php_phpdoc_type(&method.return_type);
1778 doc_lines.push(format!("@return {phpdoc_type}"));
1779 }
1780
1781 if !doc_lines.is_empty() {
1783 content.push_str(" /**\n");
1784 for line in doc_lines {
1785 content.push_str(&format!(" * {}\n", line));
1786 }
1787 content.push_str(" */\n");
1788 }
1789
1790 let static_kw = if is_static { "static " } else { "" };
1792 let first_optional_idx = method.params.iter().position(|p| p.optional);
1793 let params: Vec<String> = method
1794 .params
1795 .iter()
1796 .enumerate()
1797 .map(|(idx, p)| {
1798 let ptype = php_type(&p.ty);
1799 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1800 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1801 format!("{nullable}{ptype} ${} = null", p.name)
1802 } else {
1803 format!("{} ${}", ptype, p.name)
1804 }
1805 })
1806 .collect();
1807 content.push_str(&format!(
1808 " public {static_kw}function {method_name}({}): {return_type}\n",
1809 params.join(", ")
1810 ));
1811 let body = if is_void {
1812 " {\n }\n"
1813 } else {
1814 " {\n throw new \\RuntimeException('Not implemented — provided by the native extension.');\n }\n"
1815 };
1816 content.push_str(body);
1817 }
1818
1819 for adapter in streaming_adapters {
1821 let item_type = adapter.item_type.as_deref().unwrap_or("array");
1822 content.push_str(&gen_php_streaming_method_wrapper(adapter, item_type));
1823 content.push('\n');
1824 }
1825
1826 for bridge in trait_bridges {
1828 if let Some(ref type_alias) = bridge.type_alias {
1829 if type_alias == &typ.name {
1830 content.push_str(" /**\n");
1832 content
1833 .push_str(" * Wrap a PHP object implementing the visitor interface as a shareable handle.\n");
1834 content.push_str(" */\n");
1835 content.push_str(" public static function from_php_object(object $visitor): self\n");
1836 content.push_str(" {\n");
1837 content.push_str(
1838 " throw new \\RuntimeException('Not implemented — provided by the native extension.');\n",
1839 );
1840 content.push_str(" }\n");
1841 }
1842 }
1843 }
1844
1845 content.push_str("}\n");
1846 content
1847}
1848
1849fn gen_php_streaming_method_wrapper(adapter: &alef_core::config::AdapterConfig, _item_type: &str) -> String {
1855 let method_name = adapter.name.to_lower_camel_case();
1856
1857 let mut params_vec: Vec<String> = Vec::new();
1859
1860 for p in &adapter.params {
1861 let ptype = php_type(&alef_core::ir::TypeRef::Named(p.ty.clone()));
1862 let nullable = if p.optional { "?" } else { "" };
1863 let default = if p.optional { " = null" } else { "" };
1864 params_vec.push(format!("{nullable}{ptype} ${}{default}", p.name));
1865 }
1866
1867 let params_sig = params_vec.join(", ");
1868
1869 format!(
1874 " public function {method_name}({params_sig}): \\Generator\n {{\n \
1875 throw new \\RuntimeException('Not implemented — provided by the native extension.');\n \
1876 }}\n",
1877 method_name = method_name,
1878 )
1879}
1880
1881fn php_type(ty: &TypeRef) -> String {
1883 match ty {
1884 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1885 TypeRef::Primitive(p) => match p {
1886 PrimitiveType::Bool => "bool".to_string(),
1887 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1888 PrimitiveType::U8
1889 | PrimitiveType::U16
1890 | PrimitiveType::U32
1891 | PrimitiveType::U64
1892 | PrimitiveType::I8
1893 | PrimitiveType::I16
1894 | PrimitiveType::I32
1895 | PrimitiveType::I64
1896 | PrimitiveType::Usize
1897 | PrimitiveType::Isize => "int".to_string(),
1898 },
1899 TypeRef::Optional(inner) => {
1900 let inner_type = php_type(inner);
1903 if inner_type.starts_with('?') {
1904 inner_type
1905 } else {
1906 format!("?{inner_type}")
1907 }
1908 }
1909 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1910 TypeRef::Named(name) => name.clone(),
1911 TypeRef::Unit => "void".to_string(),
1912 TypeRef::Duration => "float".to_string(),
1913 }
1914}
1915
1916fn php_property_phpdoc(var_type: &str, doc: &str, indent: &str) -> String {
1925 let doc = doc.trim();
1926 if doc.is_empty() {
1927 return format!("{indent}/** @var {var_type} */\n");
1928 }
1929 let lines: Vec<&str> = doc.lines().collect();
1930 if lines.len() == 1 {
1931 let line = lines[0].trim();
1932 return format!("{indent}/** @var {var_type} {line} */\n");
1933 }
1934 let mut out = format!("{indent}/**\n");
1936 for line in &lines {
1937 let trimmed = line.trim();
1938 if trimmed.is_empty() {
1939 out.push_str(&format!("{indent} *\n"));
1940 } else {
1941 out.push_str(&format!("{indent} * {trimmed}\n"));
1942 }
1943 }
1944 out.push_str(&format!("{indent} *\n"));
1945 out.push_str(&format!("{indent} * @var {var_type}\n"));
1946 out.push_str(&format!("{indent} */\n"));
1947 out
1948}