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;
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 ));
329 if let Some(ctor) = config.client_constructors.get(&typ.name) {
331 let ctor_body = generators::gen_opaque_constructor(ctor, &typ.name, &core_import, "#[php_method]");
332 let ctor_impl = format!("#[php_impl]\nimpl {} {{\n{}}}", typ.name, ctor_body);
333 builder.add_item(&ctor_impl);
334 }
335 } else {
336 builder.add_item(&gen_php_struct(
339 typ,
340 &mapper,
341 &cfg,
342 Some(&php_namespace),
343 &enum_names,
344 &lang_rename_all,
345 ));
346 builder.add_item(&types::gen_struct_methods_with_exclude(
347 typ,
348 &mapper,
349 has_serde,
350 &core_import,
351 &opaque_types,
352 &enum_names,
353 &api.enums,
354 &exclude_functions,
355 &bridge_type_aliases_set,
356 &never_skip_cfg_field_names,
357 &mutex_types,
358 ));
359 }
360 }
361
362 for enum_def in &api.enums {
363 if is_tagged_data_enum(enum_def) {
364 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
366 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
367 } else {
368 builder.add_item(&gen_enum_constants(enum_def));
369 }
370 }
371
372 let included_functions: Vec<_> = api
377 .functions
378 .iter()
379 .filter(|f| !exclude_functions.contains(&f.name))
380 .collect();
381 if !included_functions.is_empty() {
382 let facade_class_name = extension_name.to_pascal_case();
383 let mut method_items: Vec<String> = Vec::new();
386 for func in included_functions {
387 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
388 if let Some((param_idx, bridge_cfg)) = bridge_param {
389 let bridge_handle_path = bridge_handle_path(api, bridge_cfg, &core_import);
390 method_items.push(crate::trait_bridge::gen_bridge_function(
391 func,
392 param_idx,
393 bridge_cfg,
394 &mapper,
395 &opaque_types,
396 &core_import,
397 &bridge_handle_path,
398 ));
399 } else if func.is_async {
400 method_items.push(gen_async_function_as_static_method(
401 func,
402 &mapper,
403 &opaque_types,
404 &core_import,
405 &config.trait_bridges,
406 &mutex_types,
407 ));
408 } else {
409 method_items.push(gen_function_as_static_method(
410 func,
411 &mapper,
412 &opaque_types,
413 &core_import,
414 &config.trait_bridges,
415 has_serde,
416 &mutex_types,
417 ));
418 }
419 }
420
421 for bridge_cfg in &config.trait_bridges {
423 if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
424 method_items.push(format!(
425 "pub fn {}(backend: &mut ext_php_rs::types::ZendObject) -> ext_php_rs::prelude::PhpResult<()> {{\n \
426 crate::{}(backend)\n}}",
427 register_fn,
428 register_fn
429 ));
430 }
431 if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
432 method_items.push(format!(
433 "pub fn {}(name: String) -> ext_php_rs::prelude::PhpResult<()> {{\n \
434 crate::{}(name)\n}}",
435 unregister_fn, unregister_fn
436 ));
437 }
438 if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
439 method_items.push(format!(
440 "pub fn {}() -> ext_php_rs::prelude::PhpResult<()> {{\n \
441 crate::{}()\n}}",
442 clear_fn, clear_fn
443 ));
444 }
445 }
446
447 let methods_joined = method_items
448 .iter()
449 .map(|m| {
450 m.lines()
452 .map(|l| {
453 if l.is_empty() {
454 String::new()
455 } else {
456 format!(" {l}")
457 }
458 })
459 .collect::<Vec<_>>()
460 .join("\n")
461 })
462 .collect::<Vec<_>>()
463 .join("\n\n");
464 let php_api_class_name = format!("{facade_class_name}Api");
467 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
469 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
470 let facade_struct = format!(
471 "#[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}}"
472 );
473 builder.add_item(&facade_struct);
474
475 for bridge_cfg in &config.trait_bridges {
477 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
478 let bridge = crate::trait_bridge::gen_trait_bridge(
479 trait_type,
480 bridge_cfg,
481 &core_import,
482 &config.error_type_name(),
483 &config.error_constructor_expr(),
484 api,
485 );
486 for imp in &bridge.imports {
487 builder.add_import(imp);
488 }
489 builder.add_item(&bridge.code);
490 }
491 }
492 }
493
494 let convertible = alef_codegen::conversions::convertible_types(api);
495 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
496 let input_types = alef_codegen::conversions::input_type_names(api);
497 let enum_names_ref = &mapper.enum_names;
502 let bridge_skip_types: Vec<String> = config
503 .trait_bridges
504 .iter()
505 .filter(|b| !matches!(b.bind_via, alef_core::config::BridgeBinding::OptionsField))
506 .filter_map(|b| b.type_alias.clone())
507 .collect();
508 let trait_bridge_arc_wrapper_field_names: Vec<String> = config
513 .trait_bridges
514 .iter()
515 .filter(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField)
516 .filter_map(|b| b.resolved_options_field().map(String::from))
517 .collect();
518 let mut conv_opaque_types: AHashSet<String> = opaque_types.clone();
523 for bridge in &config.trait_bridges {
524 if let Some(alias) = &bridge.type_alias {
525 conv_opaque_types.insert(alias.clone());
526 }
527 }
528 let php_conv_config = ConversionConfig {
529 cast_large_ints_to_i64: true,
530 enum_string_names: Some(enum_names_ref),
531 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
532 json_as_value: true,
536 include_cfg_metadata: false,
537 option_duration_on_defaults: true,
538 from_binding_skip_types: &bridge_skip_types,
539 never_skip_cfg_field_names: &never_skip_cfg_field_names,
540 opaque_types: Some(&conv_opaque_types),
541 trait_bridge_arc_wrapper_field_names: &trait_bridge_arc_wrapper_field_names,
542 ..Default::default()
543 };
544 let mut enum_tainted: AHashSet<String> = AHashSet::new();
546 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
547 if has_enum_named_field(typ, enum_names_ref) {
548 enum_tainted.insert(typ.name.clone());
549 }
550 }
551 let mut changed = true;
553 while changed {
554 changed = false;
555 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
556 if !enum_tainted.contains(&typ.name)
557 && binding_fields(&typ.fields).any(|f| references_named_type(&f.ty, &enum_tainted))
558 {
559 enum_tainted.insert(typ.name.clone());
560 changed = true;
561 }
562 }
563 }
564 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
565 if input_types.contains(&typ.name)
567 && !enum_tainted.contains(&typ.name)
568 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
569 {
570 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
571 typ,
572 &core_import,
573 &php_conv_config,
574 ));
575 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
576 builder.add_item(&gen_enum_tainted_from_binding_to_core(
583 typ,
584 &core_import,
585 enum_names_ref,
586 &enum_tainted,
587 &php_conv_config,
588 &api.enums,
589 &bridge_type_aliases_set,
590 ));
591 }
592 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
594 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
595 typ,
596 &core_import,
597 &opaque_types,
598 &php_conv_config,
599 ));
600 }
601 }
602
603 let mut emitted_binding_to_core: AHashSet<String> = api
613 .types
614 .iter()
615 .filter(|typ| !typ.is_trait && input_types.contains(&typ.name))
616 .filter(|typ| {
617 (enum_tainted.contains(&typ.name))
618 || alef_codegen::conversions::can_generate_conversion(typ, &convertible)
619 })
620 .map(|typ| typ.name.clone())
621 .collect();
622 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
623 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
624 for variant in &enum_def.variants {
627 for field in &variant.fields {
628 if let TypeRef::Named(type_name) = &field.ty {
629 if let Some(typ) = api.types.iter().find(|t| &t.name == type_name) {
630 if emitted_binding_to_core.contains(&typ.name) {
631 continue;
632 }
633 if enum_tainted.contains(&typ.name) {
634 builder.add_item(&gen_enum_tainted_from_binding_to_core(
635 typ,
636 &core_import,
637 enum_names_ref,
638 &enum_tainted,
639 &php_conv_config,
640 &api.enums,
641 &bridge_type_aliases_set,
642 ));
643 emitted_binding_to_core.insert(typ.name.clone());
644 } else if alef_codegen::conversions::can_generate_conversion(typ, &convertible) {
645 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
646 typ,
647 &core_import,
648 &php_conv_config,
649 ));
650 emitted_binding_to_core.insert(typ.name.clone());
651 }
652 }
653 }
654 }
655 }
656 }
657
658 for typ in api.types.iter().filter(|t| !t.is_trait) {
662 if !emitted_binding_to_core.contains(&typ.name) {
663 if enum_tainted.contains(&typ.name) {
664 builder.add_item(&gen_enum_tainted_from_binding_to_core(
665 typ,
666 &core_import,
667 enum_names_ref,
668 &enum_tainted,
669 &php_conv_config,
670 &api.enums,
671 &bridge_type_aliases_set,
672 ));
673 emitted_binding_to_core.insert(typ.name.clone());
674 } else if alef_codegen::conversions::can_generate_conversion(typ, &convertible) {
675 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
676 typ,
677 &core_import,
678 &php_conv_config,
679 ));
680 emitted_binding_to_core.insert(typ.name.clone());
681 }
682 }
683 }
684
685 for error in &api.errors {
687 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
688 let methods_impl = alef_codegen::error_gen::gen_php_error_methods_impl(error, &core_import);
690 if !methods_impl.is_empty() {
691 builder.add_item(&methods_impl);
692 }
693 }
694
695 if has_serde {
699 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
700 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
701 pub fn max_compression_ratio() -> i64 { 100 }\n\
702 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
703 pub fn max_nesting_depth() -> i64 { 1024 }\n\
704 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
705 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
706 pub fn max_iterations() -> i64 { 10_000_000 }\n\
707 pub fn max_xml_depth() -> i64 { 1024 }\n\
708 pub fn max_table_cells() -> i64 { 100_000 }\n\
709 }";
710 builder.add_item(serde_module);
711 }
712
713 let php_config = config.php.as_ref();
719 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
720
721 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
725 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
726 }
727
728 let mut class_registrations = String::new();
731 for typ in api
732 .types
733 .iter()
734 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
735 {
736 class_registrations.push_str(&crate::template_env::render(
737 "php_class_registration.jinja",
738 context! { class_name => &typ.name },
739 ));
740 }
741 if !api.functions.is_empty() {
743 let facade_class_name = extension_name.to_pascal_case();
744 class_registrations.push_str(&crate::template_env::render(
745 "php_class_registration.jinja",
746 context! { class_name => &format!("{facade_class_name}Api") },
747 ));
748 }
749 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
752 class_registrations.push_str(&crate::template_env::render(
753 "php_class_registration.jinja",
754 context! { class_name => &enum_def.name },
755 ));
756 }
757 for error in api.errors.iter().filter(|e| !e.methods.is_empty()) {
759 let info_class = format!("{}Info", error.name);
760 class_registrations.push_str(&crate::template_env::render(
761 "php_class_registration.jinja",
762 context! { class_name => &info_class },
763 ));
764 }
765 builder.add_item(&format!(
766 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
767 ));
768
769 let mut content = builder.build();
770
771 for bridge in &config.trait_bridges {
776 if let Some(field_name) = bridge.resolved_options_field() {
777 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
778 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
779 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
780 let builder_type = format!("{}Builder", options_type);
781 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
782 let bridge_handle_path = bridge_handle_path(api, bridge, &core_import);
783
784 let old_method = format!(
790 " 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 }}"
791 );
792 let new_method = format!(
793 " 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 }}"
794 );
795
796 content = content.replace(&old_method, &new_method);
797 }
798 }
799
800 let php_stubs_dir = config
803 .php
804 .as_ref()
805 .and_then(|p| p.stubs.as_ref())
806 .map(|s| s.output.to_string_lossy().to_string())
807 .unwrap_or_else(|| "packages/php/src/".to_string());
808
809 let php_namespace = php_autoload_namespace(config);
810
811 let mut generated_files = vec![GeneratedFile {
812 path: PathBuf::from(&output_dir).join("lib.rs"),
813 content,
814 generated_header: false,
815 }];
816
817 for bridge_cfg in &config.trait_bridges {
819 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
820 let is_visitor_bridge = bridge_cfg.type_alias.is_some()
822 && bridge_cfg.register_fn.is_none()
823 && bridge_cfg.super_trait.is_none()
824 && trait_type.methods.iter().all(|m| m.has_default_impl);
825
826 if is_visitor_bridge {
827 let interface_content = crate::trait_bridge::gen_visitor_interface(
828 trait_type,
829 bridge_cfg,
830 &php_namespace,
831 &HashMap::new(), );
833 let interface_filename = format!("{}Interface.php", bridge_cfg.trait_name);
834 generated_files.push(GeneratedFile {
835 path: PathBuf::from(&php_stubs_dir).join(&interface_filename),
836 content: interface_content,
837 generated_header: false,
838 });
839 }
840 }
841 }
842
843 Ok(generated_files)
844 }
845
846 fn generate_public_api(
847 &self,
848 api: &ApiSurface,
849 config: &ResolvedCrateConfig,
850 ) -> anyhow::Result<Vec<GeneratedFile>> {
851 let escape_phpdoc_line = |s: &str| s.replace("*/", "* /");
853
854 let extension_name = config.php_extension_name();
855 let class_name = extension_name.to_pascal_case();
856
857 let mut content = String::new();
859 content.push_str(&crate::template_env::render(
860 "php_file_header.jinja",
861 minijinja::Value::default(),
862 ));
863 content.push_str(&hash::header(CommentStyle::DoubleSlash));
864 content.push_str(&crate::template_env::render(
865 "php_declare_strict_types.jinja",
866 minijinja::Value::default(),
867 ));
868 content.push('\n');
870
871 let namespace = php_autoload_namespace(config);
873
874 content.push_str(&crate::template_env::render(
875 "php_namespace.jinja",
876 context! { namespace => &namespace },
877 ));
878 content.push('\n');
880 content.push_str(&crate::template_env::render(
881 "php_facade_class_declaration.jinja",
882 context! { class_name => &class_name },
883 ));
884
885 let bridge_param_names_pub: ahash::AHashSet<&str> = config
887 .trait_bridges
888 .iter()
889 .filter_map(|b| b.param_name.as_deref())
890 .collect();
891
892 let no_arg_constructor_types: AHashSet<String> = api
897 .types
898 .iter()
899 .filter(|t| t.fields.iter().all(|f| f.optional))
900 .map(|t| t.name.clone())
901 .collect();
902
903 for func in &api.functions {
905 let method_name = func.name.to_lower_camel_case();
910 let return_php_type = php_type(&func.return_type);
911
912 let visible_params: Vec<_> = func
914 .params
915 .iter()
916 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
917 .collect();
918
919 content.push_str(&crate::template_env::render(
921 "php_phpdoc_block_start.jinja",
922 minijinja::Value::default(),
923 ));
924 if func.doc.is_empty() {
925 content.push_str(&crate::template_env::render(
926 "php_phpdoc_text_line.jinja",
927 context! { text => &format!("{}.", method_name) },
928 ));
929 } else {
930 let sections = doc_emission::parse_rustdoc_sections(&func.doc);
932 for line in sections.summary.lines() {
934 content.push_str(" * ");
935 content.push_str(&escape_phpdoc_line(line));
936 content.push('\n');
937 }
938 }
941 content.push_str(&crate::template_env::render(
942 "php_phpdoc_empty_line.jinja",
943 minijinja::Value::default(),
944 ));
945 for p in &visible_params {
946 let ptype = php_phpdoc_type(&p.ty);
947 let nullable_prefix = if p.optional { "?" } else { "" };
948 content.push_str(&crate::template_env::render(
949 "php_phpdoc_param_line.jinja",
950 context! {
951 nullable_prefix => nullable_prefix,
952 param_type => &ptype,
953 param_name => &p.name,
954 },
955 ));
956 }
957 let return_phpdoc = php_phpdoc_type(&func.return_type);
958 content.push_str(&crate::template_env::render(
959 "php_phpdoc_return_line.jinja",
960 context! { return_type => &return_phpdoc },
961 ));
962 if func.error_type.is_some() {
963 content.push_str(&crate::template_env::render(
964 "php_phpdoc_throws_line.jinja",
965 context! {
966 namespace => namespace.as_str(),
967 class_name => &class_name,
968 },
969 ));
970 }
971 content.push_str(&crate::template_env::render(
972 "php_phpdoc_block_end.jinja",
973 minijinja::Value::default(),
974 ));
975
976 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
987 if let TypeRef::Named(name) = &p.ty {
988 (name.ends_with("Config") || name.as_str() == "config")
989 && no_arg_constructor_types.contains(name.as_str())
990 } else {
991 false
992 }
993 };
994
995 let mut first_optional_idx = None;
996 for (idx, p) in visible_params.iter().enumerate() {
997 if p.optional || is_optional_config_param(p) {
998 first_optional_idx = Some(idx);
999 break;
1000 }
1001 }
1002
1003 content.push_str(&crate::template_env::render(
1004 "php_method_signature_start.jinja",
1005 context! { method_name => &method_name },
1006 ));
1007
1008 let params: Vec<String> = visible_params
1009 .iter()
1010 .enumerate()
1011 .map(|(idx, p)| {
1012 let ptype = php_type(&p.ty);
1013 let should_be_optional = p.optional
1018 || is_optional_config_param(p)
1019 || first_optional_idx.is_some_and(|first| idx >= first);
1020 if should_be_optional {
1021 format!("?{} ${} = null", ptype, p.name)
1022 } else {
1023 format!("{} ${}", ptype, p.name)
1024 }
1025 })
1026 .collect();
1027 content.push_str(¶ms.join(", "));
1028 content.push_str(&crate::template_env::render(
1029 "php_method_signature_end.jinja",
1030 context! { return_type => &return_php_type },
1031 ));
1032 let ext_method_name = func.name.to_lower_camel_case();
1037 let is_void = matches!(&func.return_type, TypeRef::Unit);
1038 let call_params = visible_params
1046 .iter()
1047 .enumerate()
1048 .map(|(idx, p)| {
1049 let should_be_optional = p.optional
1050 || is_optional_config_param(p)
1051 || first_optional_idx.is_some_and(|first| idx >= first);
1052 if should_be_optional && is_optional_config_param(p) {
1053 if let TypeRef::Named(type_name) = &p.ty {
1054 return format!("${} ?? new {}()", p.name, type_name);
1055 }
1056 }
1057 format!("${}", p.name)
1058 })
1059 .collect::<Vec<_>>()
1060 .join(", ");
1061 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
1062 if is_void {
1063 content.push_str(&crate::template_env::render(
1064 "php_method_call_statement.jinja",
1065 context! { call_expr => &call_expr },
1066 ));
1067 } else {
1068 content.push_str(&crate::template_env::render(
1069 "php_method_call_return.jinja",
1070 context! { call_expr => &call_expr },
1071 ));
1072 }
1073 content.push_str(&crate::template_env::render(
1074 "php_method_end.jinja",
1075 minijinja::Value::default(),
1076 ));
1077 }
1078
1079 for bridge_cfg in &config.trait_bridges {
1081 if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
1082 let method_name = register_fn.to_lower_camel_case();
1083 content.push_str(&crate::template_env::render(
1084 "php_phpdoc_block_start.jinja",
1085 minijinja::Value::default(),
1086 ));
1087 content.push_str(&crate::template_env::render(
1088 "php_phpdoc_text_line.jinja",
1089 context! { text => &format!("{}.", method_name) },
1090 ));
1091 content.push_str(&crate::template_env::render(
1092 "php_phpdoc_empty_line.jinja",
1093 minijinja::Value::default(),
1094 ));
1095 let interface_name = &bridge_cfg.trait_name;
1096 content.push_str(&crate::template_env::render(
1097 "php_phpdoc_param_line.jinja",
1098 context! {
1099 nullable_prefix => "",
1100 param_type => interface_name,
1101 param_name => "backend",
1102 },
1103 ));
1104 content.push_str(&crate::template_env::render(
1105 "php_phpdoc_return_line.jinja",
1106 context! { return_type => "void" },
1107 ));
1108 content.push_str(&crate::template_env::render(
1109 "php_phpdoc_block_end.jinja",
1110 minijinja::Value::default(),
1111 ));
1112 content.push_str(&crate::template_env::render(
1113 "php_method_signature_start.jinja",
1114 context! { method_name => &method_name },
1115 ));
1116 content.push_str(&format!("{} $backend = null) : void\n {{\n", interface_name));
1117 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}($backend)");
1118 content.push_str(&crate::template_env::render(
1119 "php_method_call_statement.jinja",
1120 context! { call_expr => &call_expr },
1121 ));
1122 content.push_str(&crate::template_env::render(
1123 "php_method_end.jinja",
1124 minijinja::Value::default(),
1125 ));
1126 }
1127 if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
1128 let method_name = unregister_fn.to_lower_camel_case();
1129 content.push_str(&crate::template_env::render(
1130 "php_phpdoc_block_start.jinja",
1131 minijinja::Value::default(),
1132 ));
1133 content.push_str(&crate::template_env::render(
1134 "php_phpdoc_text_line.jinja",
1135 context! { text => &format!("{}.", method_name) },
1136 ));
1137 content.push_str(&crate::template_env::render(
1138 "php_phpdoc_empty_line.jinja",
1139 minijinja::Value::default(),
1140 ));
1141 content.push_str(&crate::template_env::render(
1142 "php_phpdoc_param_line.jinja",
1143 context! {
1144 nullable_prefix => "",
1145 param_type => "string",
1146 param_name => "name",
1147 },
1148 ));
1149 content.push_str(&crate::template_env::render(
1150 "php_phpdoc_return_line.jinja",
1151 context! { return_type => "void" },
1152 ));
1153 content.push_str(&crate::template_env::render(
1154 "php_phpdoc_block_end.jinja",
1155 minijinja::Value::default(),
1156 ));
1157 content.push_str(&crate::template_env::render(
1158 "php_method_signature_start.jinja",
1159 context! { method_name => &method_name },
1160 ));
1161 content.push_str("string $name) : void\n {\n");
1162 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}($name)");
1163 content.push_str(&crate::template_env::render(
1164 "php_method_call_statement.jinja",
1165 context! { call_expr => &call_expr },
1166 ));
1167 content.push_str(&crate::template_env::render(
1168 "php_method_end.jinja",
1169 minijinja::Value::default(),
1170 ));
1171 }
1172 if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
1173 let method_name = clear_fn.to_lower_camel_case();
1174 content.push_str(&crate::template_env::render(
1175 "php_phpdoc_block_start.jinja",
1176 minijinja::Value::default(),
1177 ));
1178 content.push_str(&crate::template_env::render(
1179 "php_phpdoc_text_line.jinja",
1180 context! { text => &format!("{}.", method_name) },
1181 ));
1182 content.push_str(&crate::template_env::render(
1183 "php_phpdoc_empty_line.jinja",
1184 minijinja::Value::default(),
1185 ));
1186 content.push_str(&crate::template_env::render(
1187 "php_phpdoc_return_line.jinja",
1188 context! { return_type => "void" },
1189 ));
1190 content.push_str(&crate::template_env::render(
1191 "php_phpdoc_block_end.jinja",
1192 minijinja::Value::default(),
1193 ));
1194 content.push_str(&crate::template_env::render(
1195 "php_method_signature_start.jinja",
1196 context! { method_name => &method_name },
1197 ));
1198 content.push_str(") : void\n {\n");
1199 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}()");
1200 content.push_str(&crate::template_env::render(
1201 "php_method_call_statement.jinja",
1202 context! { call_expr => &call_expr },
1203 ));
1204 content.push_str(&crate::template_env::render(
1205 "php_method_end.jinja",
1206 minijinja::Value::default(),
1207 ));
1208 }
1209 }
1210
1211 content.push_str(&crate::template_env::render(
1212 "php_class_end.jinja",
1213 minijinja::Value::default(),
1214 ));
1215
1216 let output_dir = config
1220 .php
1221 .as_ref()
1222 .and_then(|p| p.stubs.as_ref())
1223 .map(|s| s.output.to_string_lossy().to_string())
1224 .unwrap_or_else(|| "packages/php/src/".to_string());
1225
1226 let mut files: Vec<GeneratedFile> = Vec::new();
1227 files.push(GeneratedFile {
1228 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
1229 content,
1230 generated_header: false,
1231 });
1232
1233 for typ in api.types.iter().filter(|t| t.is_opaque && !t.is_trait) {
1239 let streaming_adapters: Vec<&alef_core::config::AdapterConfig> = config
1240 .adapters
1241 .iter()
1242 .filter(|a| {
1243 matches!(a.pattern, alef_core::config::AdapterPattern::Streaming)
1244 && a.owner_type.as_deref() == Some(&typ.name)
1245 && !a.skip_languages.iter().any(|l| l == "php")
1246 })
1247 .collect();
1248 let streaming_method_names: AHashSet<String> = streaming_adapters.iter().map(|a| a.name.clone()).collect();
1249 let opaque_file = gen_php_opaque_class_file(typ, &namespace, &streaming_adapters, &streaming_method_names);
1250 files.push(GeneratedFile {
1251 path: PathBuf::from(&output_dir).join(format!("{}.php", typ.name)),
1252 content: opaque_file,
1253 generated_header: false,
1254 });
1255 }
1256
1257 Ok(files)
1258 }
1259
1260 fn generate_type_stubs(
1261 &self,
1262 api: &ApiSurface,
1263 config: &ResolvedCrateConfig,
1264 ) -> anyhow::Result<Vec<GeneratedFile>> {
1265 let extension_name = config.php_extension_name();
1266 let class_name = extension_name.to_pascal_case();
1267
1268 let namespace = php_autoload_namespace(config);
1270
1271 let mut content = String::new();
1276 content.push_str(&crate::template_env::render(
1277 "php_file_header.jinja",
1278 minijinja::Value::default(),
1279 ));
1280 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1281 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
1282 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
1283 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
1284 content.push_str(&crate::template_env::render(
1285 "php_declare_strict_types.jinja",
1286 minijinja::Value::default(),
1287 ));
1288 content.push('\n');
1290 content.push_str(&crate::template_env::render(
1292 "php_namespace_block_begin.jinja",
1293 context! { namespace => &namespace },
1294 ));
1295
1296 content.push_str(&crate::template_env::render(
1298 "php_exception_class_declaration.jinja",
1299 context! { class_name => &class_name },
1300 ));
1301 content.push_str(
1302 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1303 );
1304 let has_status_code = api
1307 .errors
1308 .iter()
1309 .any(|e| e.methods.iter().any(|m| m.name == "status_code"));
1310 let has_is_transient = api
1311 .errors
1312 .iter()
1313 .any(|e| e.methods.iter().any(|m| m.name == "is_transient"));
1314 let has_error_type = api
1315 .errors
1316 .iter()
1317 .any(|e| e.methods.iter().any(|m| m.name == "error_type"));
1318 if has_status_code {
1319 content.push_str(
1320 " /** HTTP status code for this error (0 means no associated status). */\n \
1321 public function statusCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1322 );
1323 }
1324 if has_is_transient {
1325 content.push_str(
1326 " /** Returns true if the error is transient and a retry may succeed. */\n \
1327 public function isTransient(): bool { throw new \\RuntimeException('Not implemented.'); }\n",
1328 );
1329 }
1330 if has_error_type {
1331 content.push_str(
1332 " /** Machine-readable error category string for matching and logging. */\n \
1333 public function errorType(): string { throw new \\RuntimeException('Not implemented.'); }\n",
1334 );
1335 }
1336 content.push_str("}\n\n");
1337
1338 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
1345 if typ.is_opaque || typ.fields.is_empty() {
1346 continue;
1347 }
1348 if !typ.doc.is_empty() {
1349 content.push_str("/**\n");
1350 content.push_str(&crate::template_env::render(
1351 "php_phpdoc_lines.jinja",
1352 context! {
1353 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
1354 indent => "",
1355 },
1356 ));
1357 content.push_str(" */\n");
1358 }
1359 content.push_str(&crate::template_env::render(
1360 "php_record_class_stub_declaration.jinja",
1361 context! { class_name => &typ.name },
1362 ));
1363
1364 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = binding_fields(&typ.fields).collect();
1367 sorted_fields.sort_by_key(|f| f.optional);
1368
1369 let params: Vec<String> = sorted_fields
1374 .iter()
1375 .map(|f| {
1376 let ptype = php_type(&f.ty);
1377 let nullable = if f.optional && !ptype.starts_with('?') {
1378 format!("?{ptype}")
1379 } else {
1380 ptype
1381 };
1382 let default = if f.optional { " = null" } else { "" };
1383 let php_name = to_php_name(&f.name);
1384 let phpdoc_type = php_phpdoc_type(&f.ty);
1385 let var_type = if f.optional && !phpdoc_type.starts_with('?') {
1386 format!("?{phpdoc_type}")
1387 } else {
1388 phpdoc_type
1389 };
1390 let phpdoc = php_property_phpdoc(&var_type, &f.doc, " ");
1391 format!("{phpdoc} public readonly {nullable} ${php_name}{default}",)
1392 })
1393 .collect();
1394 content.push_str(&crate::template_env::render(
1395 "php_constructor_method.jinja",
1396 context! { params => ¶ms.join(",\n") },
1397 ));
1398
1399 let non_excluded_methods: Vec<&alef_core::ir::MethodDef> = typ
1404 .methods
1405 .iter()
1406 .filter(|m| !m.binding_excluded && !m.sanitized)
1407 .collect();
1408 for method in non_excluded_methods {
1409 let method_name = method.name.to_lower_camel_case();
1410 let is_static = method.receiver.is_none();
1411 let return_type = php_type(&method.return_type);
1412 let first_optional_idx = method.params.iter().position(|p| p.optional);
1413 let params: Vec<String> = method
1414 .params
1415 .iter()
1416 .enumerate()
1417 .map(|(idx, p)| {
1418 let ptype = php_type(&p.ty);
1419 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1420 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1421 format!("{nullable}{ptype} ${} = null", p.name)
1422 } else {
1423 format!("{} ${}", ptype, p.name)
1424 }
1425 })
1426 .collect();
1427 let static_kw = if is_static { "static " } else { "" };
1428 let is_void = matches!(&method.return_type, TypeRef::Unit);
1429 let stub_body = if is_void {
1430 "{ }".to_string()
1431 } else {
1432 "{ throw new \\RuntimeException('Not implemented — provided by the native extension.'); }"
1433 .to_string()
1434 };
1435 content.push_str(&format!(
1436 " public {static_kw}function {method_name}({}): {return_type}\n {stub_body}\n",
1437 params.join(", ")
1438 ));
1439 }
1440
1441 content.push_str("}\n\n");
1442 }
1443
1444 for enum_def in &api.enums {
1447 if is_tagged_data_enum(enum_def) {
1448 if !enum_def.doc.is_empty() {
1450 content.push_str("/**\n");
1451 content.push_str(&crate::template_env::render(
1452 "php_phpdoc_lines.jinja",
1453 context! {
1454 doc_lines => enum_def.doc.lines().collect::<Vec<_>>(),
1455 indent => "",
1456 },
1457 ));
1458 content.push_str(" */\n");
1459 }
1460 content.push_str(&crate::template_env::render(
1461 "php_record_class_stub_declaration.jinja",
1462 context! { class_name => &enum_def.name },
1463 ));
1464 content.push_str("}\n\n");
1465 } else {
1466 content.push_str(&crate::template_env::render(
1468 "php_tagged_enum_declaration.jinja",
1469 context! { enum_name => &enum_def.name },
1470 ));
1471 for variant in &enum_def.variants {
1472 let case_name = sanitize_php_enum_case(&variant.name);
1473 content.push_str(&crate::template_env::render(
1474 "php_enum_variant_stub.jinja",
1475 context! {
1476 variant_name => case_name,
1477 value => &variant.name,
1478 },
1479 ));
1480 }
1481 content.push_str("}\n\n");
1482 }
1483 }
1484
1485 if !api.functions.is_empty() {
1490 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1492 .trait_bridges
1493 .iter()
1494 .filter_map(|b| b.param_name.as_deref())
1495 .collect();
1496
1497 content.push_str(&crate::template_env::render(
1498 "php_api_class_declaration.jinja",
1499 context! { class_name => &class_name },
1500 ));
1501 for func in &api.functions {
1502 let return_type = php_type_fq(&func.return_type, &namespace);
1503 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1504 let visible_params: Vec<_> = func
1506 .params
1507 .iter()
1508 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1509 .collect();
1510 let has_array_params = visible_params
1517 .iter()
1518 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1519 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1520 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1521 let first_optional_idx = visible_params.iter().position(|p| p.optional);
1522 if has_array_params || has_array_return {
1523 content.push_str(" /**\n");
1524 for (idx, p) in visible_params.iter().enumerate() {
1525 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1526 let nullable_prefix = if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1527 "?"
1528 } else {
1529 ""
1530 };
1531 content.push_str(&crate::template_env::render(
1532 "php_phpdoc_static_param.jinja",
1533 context! {
1534 nullable_prefix => nullable_prefix,
1535 ptype => &ptype,
1536 param_name => &p.name,
1537 },
1538 ));
1539 }
1540 content.push_str(&crate::template_env::render(
1541 "php_phpdoc_static_return.jinja",
1542 context! { return_phpdoc => &return_phpdoc },
1543 ));
1544 content.push_str(" */\n");
1545 }
1546 let params: Vec<String> = visible_params
1547 .iter()
1548 .enumerate()
1549 .map(|(idx, p)| {
1550 let ptype = php_type_fq(&p.ty, &namespace);
1551 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1552 let nullable_ptype = if ptype.starts_with('?') {
1553 ptype
1554 } else {
1555 format!("?{ptype}")
1556 };
1557 format!("{} ${} = null", nullable_ptype, p.name)
1558 } else {
1559 format!("{} ${}", ptype, p.name)
1560 }
1561 })
1562 .collect();
1563 let stub_method_name = func.name.to_lower_camel_case();
1567 let is_void_stub = return_type == "void";
1568 let stub_body = if is_void_stub {
1569 "{ }".to_string()
1570 } else {
1571 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1572 };
1573 content.push_str(&crate::template_env::render(
1574 "php_static_method_stub.jinja",
1575 context! {
1576 method_name => &stub_method_name,
1577 params => ¶ms.join(", "),
1578 return_type => &return_type,
1579 stub_body => &stub_body,
1580 },
1581 ));
1582 }
1583 content.push_str("}\n\n");
1584 }
1585
1586 content.push_str(&crate::template_env::render(
1588 "php_namespace_block_end.jinja",
1589 minijinja::Value::default(),
1590 ));
1591
1592 let output_dir = config
1594 .php
1595 .as_ref()
1596 .and_then(|p| p.stubs.as_ref())
1597 .map(|s| s.output.to_string_lossy().to_string())
1598 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1599
1600 Ok(vec![GeneratedFile {
1601 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1602 content,
1603 generated_header: false,
1604 }])
1605 }
1606
1607 fn build_config(&self) -> Option<BuildConfig> {
1608 Some(BuildConfig {
1609 tool: "cargo",
1610 crate_suffix: "-php",
1611 build_dep: BuildDependency::None,
1612 post_build: vec![],
1613 })
1614 }
1615}
1616
1617fn bridge_handle_path(api: &ApiSurface, bridge: &alef_core::config::TraitBridgeConfig, core_import: &str) -> String {
1618 let alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
1619 api.types
1620 .iter()
1621 .find(|t| t.name == alias && !t.rust_path.is_empty())
1622 .map(|t| t.rust_path.replace('-', "_"))
1623 .or_else(|| api.excluded_type_paths.get(alias).map(|path| path.replace('-', "_")))
1624 .unwrap_or_else(|| format!("{core_import}::visitor::{alias}"))
1625}
1626
1627fn php_phpdoc_type(ty: &TypeRef) -> String {
1630 match ty {
1631 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1632 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1633 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1634 _ => php_type(ty),
1635 }
1636}
1637
1638fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1640 match ty {
1641 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1642 TypeRef::Map(k, v) => format!(
1643 "array<{}, {}>",
1644 php_phpdoc_type_fq(k, namespace),
1645 php_phpdoc_type_fq(v, namespace)
1646 ),
1647 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1648 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1649 _ => php_type(ty),
1650 }
1651}
1652
1653fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1655 match ty {
1656 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1657 TypeRef::Optional(inner) => {
1658 let inner_type = php_type_fq(inner, namespace);
1659 if inner_type.starts_with('?') {
1660 inner_type
1661 } else {
1662 format!("?{inner_type}")
1663 }
1664 }
1665 _ => php_type(ty),
1666 }
1667}
1668
1669fn gen_php_opaque_class_file(
1676 typ: &alef_core::ir::TypeDef,
1677 namespace: &str,
1678 streaming_adapters: &[&alef_core::config::AdapterConfig],
1679 streaming_method_names: &AHashSet<String>,
1680) -> String {
1681 let mut content = String::new();
1682 content.push_str(&crate::template_env::render(
1683 "php_file_header.jinja",
1684 minijinja::Value::default(),
1685 ));
1686 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1687 content.push_str(&crate::template_env::render(
1688 "php_declare_strict_types.jinja",
1689 minijinja::Value::default(),
1690 ));
1691 content.push('\n');
1693 content.push_str(&crate::template_env::render(
1694 "php_namespace.jinja",
1695 context! { namespace => namespace },
1696 ));
1697 content.push('\n');
1699
1700 if !typ.doc.is_empty() {
1702 content.push_str("/**\n");
1703 content.push_str(&crate::template_env::render(
1704 "php_phpdoc_lines.jinja",
1705 context! {
1706 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
1707 indent => "",
1708 },
1709 ));
1710 content.push_str(" */\n");
1711 }
1712
1713 content.push_str(&format!("final class {}\n{{\n", typ.name));
1714
1715 let mut method_order: Vec<&alef_core::ir::MethodDef> = Vec::new();
1718 method_order.extend(
1719 typ.methods
1720 .iter()
1721 .filter(|m| m.receiver.is_some() && !streaming_method_names.contains(&m.name)),
1722 );
1723 method_order.extend(
1724 typ.methods
1725 .iter()
1726 .filter(|m| m.receiver.is_none() && !streaming_method_names.contains(&m.name)),
1727 );
1728
1729 for method in method_order {
1730 let method_name = method.name.to_lower_camel_case();
1731 let return_type = php_type(&method.return_type);
1732 let is_void = matches!(&method.return_type, TypeRef::Unit);
1733 let is_static = method.receiver.is_none();
1734
1735 let mut doc_lines: Vec<String> = vec![];
1737 let doc_line = method.doc.lines().next().unwrap_or("").trim();
1738 if !doc_line.is_empty() {
1739 doc_lines.push(doc_line.to_string());
1740 }
1741
1742 let mut phpdoc_params: Vec<String> = vec![];
1744 for param in &method.params {
1745 if matches!(¶m.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)) {
1746 let phpdoc_type = php_phpdoc_type(¶m.ty);
1747 phpdoc_params.push(format!("@param {} ${}", phpdoc_type, param.name));
1748 }
1749 }
1750 doc_lines.extend(phpdoc_params);
1751
1752 let needs_return_phpdoc = matches!(&method.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _));
1754 if needs_return_phpdoc {
1755 let phpdoc_type = php_phpdoc_type(&method.return_type);
1756 doc_lines.push(format!("@return {phpdoc_type}"));
1757 }
1758
1759 if !doc_lines.is_empty() {
1761 content.push_str(" /**\n");
1762 for line in doc_lines {
1763 content.push_str(&format!(" * {}\n", line));
1764 }
1765 content.push_str(" */\n");
1766 }
1767
1768 let static_kw = if is_static { "static " } else { "" };
1770 let first_optional_idx = method.params.iter().position(|p| p.optional);
1771 let params: Vec<String> = method
1772 .params
1773 .iter()
1774 .enumerate()
1775 .map(|(idx, p)| {
1776 let ptype = php_type(&p.ty);
1777 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1778 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1779 format!("{nullable}{ptype} ${} = null", p.name)
1780 } else {
1781 format!("{} ${}", ptype, p.name)
1782 }
1783 })
1784 .collect();
1785 content.push_str(&format!(
1786 " public {static_kw}function {method_name}({}): {return_type}\n",
1787 params.join(", ")
1788 ));
1789 let body = if is_void {
1790 " {\n }\n"
1791 } else {
1792 " {\n throw new \\RuntimeException('Not implemented — provided by the native extension.');\n }\n"
1793 };
1794 content.push_str(body);
1795 }
1796
1797 for adapter in streaming_adapters {
1799 let item_type = adapter.item_type.as_deref().unwrap_or("array");
1800 content.push_str(&gen_php_streaming_method_wrapper(adapter, item_type));
1801 content.push('\n');
1802 }
1803
1804 content.push_str("}\n");
1805 content
1806}
1807
1808fn gen_php_streaming_method_wrapper(adapter: &alef_core::config::AdapterConfig, _item_type: &str) -> String {
1814 let method_name = adapter.name.to_lower_camel_case();
1815
1816 let mut params_vec: Vec<String> = Vec::new();
1818
1819 for p in &adapter.params {
1820 let ptype = php_type(&alef_core::ir::TypeRef::Named(p.ty.clone()));
1821 let nullable = if p.optional { "?" } else { "" };
1822 let default = if p.optional { " = null" } else { "" };
1823 params_vec.push(format!("{nullable}{ptype} ${}{default}", p.name));
1824 }
1825
1826 let params_sig = params_vec.join(", ");
1827
1828 format!(
1833 " public function {method_name}({params_sig}): \\Generator\n {{\n \
1834 throw new \\RuntimeException('Not implemented — provided by the native extension.');\n \
1835 }}\n",
1836 method_name = method_name,
1837 )
1838}
1839
1840fn php_type(ty: &TypeRef) -> String {
1842 match ty {
1843 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1844 TypeRef::Primitive(p) => match p {
1845 PrimitiveType::Bool => "bool".to_string(),
1846 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1847 PrimitiveType::U8
1848 | PrimitiveType::U16
1849 | PrimitiveType::U32
1850 | PrimitiveType::U64
1851 | PrimitiveType::I8
1852 | PrimitiveType::I16
1853 | PrimitiveType::I32
1854 | PrimitiveType::I64
1855 | PrimitiveType::Usize
1856 | PrimitiveType::Isize => "int".to_string(),
1857 },
1858 TypeRef::Optional(inner) => {
1859 let inner_type = php_type(inner);
1862 if inner_type.starts_with('?') {
1863 inner_type
1864 } else {
1865 format!("?{inner_type}")
1866 }
1867 }
1868 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1869 TypeRef::Named(name) => name.clone(),
1870 TypeRef::Unit => "void".to_string(),
1871 TypeRef::Duration => "float".to_string(),
1872 }
1873}
1874
1875fn php_property_phpdoc(var_type: &str, doc: &str, indent: &str) -> String {
1884 let doc = doc.trim();
1885 if doc.is_empty() {
1886 return format!("{indent}/** @var {var_type} */\n");
1887 }
1888 let lines: Vec<&str> = doc.lines().collect();
1889 if lines.len() == 1 {
1890 let line = lines[0].trim();
1891 return format!("{indent}/** @var {var_type} {line} */\n");
1892 }
1893 let mut out = format!("{indent}/**\n");
1895 for line in &lines {
1896 let trimmed = line.trim();
1897 if trimmed.is_empty() {
1898 out.push_str(&format!("{indent} *\n"));
1899 } else {
1900 out.push_str(&format!("{indent} * {trimmed}\n"));
1901 }
1902 }
1903 out.push_str(&format!("{indent} *\n"));
1904 out.push_str(&format!("{indent} * @var {var_type}\n"));
1905 out.push_str(&format!("{indent} */\n"));
1906 out
1907}