1mod functions;
2mod helpers;
3pub mod types;
4
5use crate::type_map::PhpMapper;
6use ahash::AHashSet;
7use alef_codegen::builder::RustFileBuilder;
8use alef_codegen::conversions::ConversionConfig;
9use alef_codegen::generators::RustBindingConfig;
10use alef_codegen::generators::{self, AsyncPattern};
11use alef_codegen::naming::to_php_name;
12use alef_codegen::shared::binding_fields;
13use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
14use alef_core::config::{Language, ResolvedCrateConfig, detect_serde_available, resolve_output_dir};
15use alef_core::hash::{self, CommentStyle};
16use alef_core::ir::ApiSurface;
17use alef_core::ir::{PrimitiveType, TypeRef};
18use heck::{ToLowerCamelCase, ToPascalCase};
19use minijinja::context;
20use std::collections::HashMap;
21use std::path::PathBuf;
22
23use crate::naming::php_autoload_namespace;
24use functions::{gen_async_function_as_static_method, gen_function_as_static_method};
25
26fn sanitize_php_enum_case(name: &str) -> String {
29 if name.eq_ignore_ascii_case("class") {
30 format!("{name}_")
31 } else {
32 name.to_string()
33 }
34}
35use helpers::{gen_enum_tainted_from_binding_to_core, gen_tokio_runtime, has_enum_named_field, references_named_type};
36use types::{
37 gen_enum_constants, gen_flat_data_enum, gen_flat_data_enum_from_impls, gen_flat_data_enum_methods, gen_php_struct,
38 is_tagged_data_enum, is_untagged_data_enum,
39};
40
41pub struct PhpBackend;
42
43impl PhpBackend {
44 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
45 RustBindingConfig {
46 struct_attrs: &["php_class"],
47 field_attrs: &[],
48 struct_derives: &["Clone"],
49 method_block_attr: Some("php_impl"),
50 constructor_attr: "",
51 static_attr: None,
52 function_attr: "#[php_function]",
53 enum_attrs: &[],
54 enum_derives: &[],
55 needs_signature: false,
56 signature_prefix: "",
57 signature_suffix: "",
58 core_import,
59 async_pattern: AsyncPattern::TokioBlockOn,
60 has_serde,
61 type_name_prefix: "",
62 option_duration_on_defaults: true,
63 opaque_type_names: &[],
64 skip_impl_constructor: false,
65 cast_uints_to_i32: false,
66 cast_large_ints_to_f64: false,
67 named_non_opaque_params_by_ref: false,
68 lossy_skip_types: &[],
69 serializable_opaque_type_names: &[],
70 never_skip_cfg_field_names: &[],
71 }
72 }
73}
74
75impl Backend for PhpBackend {
76 fn name(&self) -> &str {
77 "php"
78 }
79
80 fn language(&self) -> Language {
81 Language::Php
82 }
83
84 fn capabilities(&self) -> Capabilities {
85 Capabilities {
86 supports_async: false,
87 supports_classes: true,
88 supports_enums: true,
89 supports_option: true,
90 supports_result: true,
91 ..Capabilities::default()
92 }
93 }
94
95 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
96 let data_enum_names: AHashSet<String> = api
99 .enums
100 .iter()
101 .filter(|e| is_tagged_data_enum(e))
102 .map(|e| e.name.clone())
103 .collect();
104 let untagged_data_enum_names: AHashSet<String> = api
105 .enums
106 .iter()
107 .filter(|e| is_untagged_data_enum(e))
108 .map(|e| e.name.clone())
109 .collect();
110 let enum_names: AHashSet<String> = api
113 .enums
114 .iter()
115 .filter(|e| !is_tagged_data_enum(e) && !is_untagged_data_enum(e))
116 .map(|e| e.name.clone())
117 .collect();
118 let mapper = PhpMapper {
119 enum_names: enum_names.clone(),
120 data_enum_names: data_enum_names.clone(),
121 untagged_data_enum_names: untagged_data_enum_names.clone(),
122 };
123 let core_import = config.core_import_name();
124 let lang_rename_all = config.serde_rename_all_for_language(Language::Php);
125
126 let php_config = config.php.as_ref();
128 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
129 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
130
131 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
132 let has_serde = detect_serde_available(&output_dir);
133
134 let bridge_type_aliases_php: Vec<String> = config
140 .trait_bridges
141 .iter()
142 .filter_map(|b| b.type_alias.clone())
143 .collect();
144 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
145 let mut opaque_names_vec_php: Vec<String> = api
146 .types
147 .iter()
148 .filter(|t| t.is_opaque)
149 .map(|t| t.name.clone())
150 .collect();
151 opaque_names_vec_php.extend(bridge_type_aliases_php);
152
153 let mut cfg = Self::binding_config(&core_import, has_serde);
154 cfg.opaque_type_names = &opaque_names_vec_php;
155 let never_skip_cfg_field_names: Vec<String> = config
156 .trait_bridges
157 .iter()
158 .filter_map(|b| {
159 if b.bind_via == alef_core::config::BridgeBinding::OptionsField {
160 b.resolved_options_field().map(|s| s.to_string())
161 } else {
162 None
163 }
164 })
165 .collect();
166 cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;
167
168 let mut builder = RustFileBuilder::new().with_generated_header();
170 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
171 builder.add_inner_attribute("allow(unsafe_code)");
172 builder.add_inner_attribute("allow(non_snake_case)");
174 builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions, clippy::arc_with_non_send_sync, clippy::collapsible_if, clippy::clone_on_copy, clippy::should_implement_trait, clippy::useless_conversion)");
175 builder.add_import("ext_php_rs::prelude::*");
176
177 if has_serde {
179 builder.add_import("serde_json");
180 }
181
182 for trait_path in generators::collect_trait_imports(api) {
184 builder.add_import(&trait_path);
185 }
186
187 let has_maps = api.types.iter().any(|t| {
189 t.fields
190 .iter()
191 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
192 }) || api
193 .functions
194 .iter()
195 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
196 if has_maps {
197 builder.add_import("std::collections::HashMap");
198 }
199
200 builder.add_item(
205 "#[derive(Debug, Clone, Default)]\n\
206 pub struct PhpBytes(pub Vec<u8>);\n\
207 \n\
208 impl<'a> ext_php_rs::convert::FromZval<'a> for PhpBytes {\n \
209 const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::String;\n \
210 fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {\n \
211 zval.zend_str().map(|zs| PhpBytes(zs.as_bytes().to_vec()))\n \
212 }\n\
213 }\n\
214 \n\
215 impl From<PhpBytes> for Vec<u8> {\n \
216 fn from(b: PhpBytes) -> Self { b.0 }\n\
217 }\n\
218 \n\
219 impl From<Vec<u8>> for PhpBytes {\n \
220 fn from(v: Vec<u8>) -> Self { PhpBytes(v) }\n\
221 }\n",
222 );
223
224 let custom_mods = config.custom_modules.for_language(Language::Php);
226 for module in custom_mods {
227 builder.add_item(&format!("pub mod {module};"));
228 }
229
230 let has_async =
232 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
233
234 if has_async {
235 builder.add_item(&gen_tokio_runtime());
236 }
237
238 let opaque_types: AHashSet<String> = api
240 .types
241 .iter()
242 .filter(|t| t.is_opaque)
243 .map(|t| t.name.clone())
244 .collect();
245 if !opaque_types.is_empty() {
246 builder.add_import("std::sync::Arc");
247 }
248
249 let mutex_types: AHashSet<String> = api
251 .types
252 .iter()
253 .filter(|t| t.is_opaque && alef_codegen::generators::type_needs_mutex(t))
254 .map(|t| t.name.clone())
255 .collect();
256 if !mutex_types.is_empty() {
257 builder.add_import("std::sync::Mutex");
258 }
259
260 let extension_name = config.php_extension_name();
263 let php_namespace = php_autoload_namespace(config);
264
265 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
267
268 let streaming_method_keys: AHashSet<String> = config
273 .adapters
274 .iter()
275 .filter(|a| matches!(a.pattern, alef_core::config::AdapterPattern::Streaming))
276 .filter_map(|a| a.owner_type.as_deref().map(|owner| format!("{owner}.{}", a.name)))
277 .collect();
278
279 for adapter in &config.adapters {
281 match adapter.pattern {
282 alef_core::config::AdapterPattern::Streaming => {
283 let key = alef_adapters::stream_struct_key(adapter);
284 if let Some(struct_code) = adapter_bodies.get(&key) {
285 builder.add_item(struct_code);
286 }
287 }
288 alef_core::config::AdapterPattern::CallbackBridge => {
289 let struct_key = format!("{}.__bridge_struct__", adapter.name);
290 let impl_key = format!("{}.__bridge_impl__", adapter.name);
291 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
292 builder.add_item(struct_code);
293 }
294 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
295 builder.add_item(impl_code);
296 }
297 }
298 _ => {}
299 }
300 }
301
302 for typ in api
303 .types
304 .iter()
305 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
306 {
307 if typ.is_opaque {
308 let ns_escaped = php_namespace.replace('\\', "\\\\");
312 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
313 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
314 let opaque_cfg = RustBindingConfig {
315 struct_attrs: &opaque_attr_arr,
316 ..cfg
317 };
318 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
319 builder.add_item(&types::gen_opaque_struct_methods_with_exclude(
320 typ,
321 &mapper,
322 &opaque_types,
323 &core_import,
324 &adapter_bodies,
325 &mutex_types,
326 &streaming_method_keys,
327 ));
328 if let Some(ctor) = config.client_constructors.get(&typ.name) {
330 let ctor_body = generators::gen_opaque_constructor(ctor, &typ.name, &core_import, "#[php_method]");
331 let ctor_impl = format!("#[php_impl]\nimpl {} {{\n{}}}", typ.name, ctor_body);
332 builder.add_item(&ctor_impl);
333 }
334 } else {
335 builder.add_item(&gen_php_struct(
338 typ,
339 &mapper,
340 &cfg,
341 Some(&php_namespace),
342 &enum_names,
343 &lang_rename_all,
344 ));
345 builder.add_item(&types::gen_struct_methods_with_exclude(
346 typ,
347 &mapper,
348 has_serde,
349 &core_import,
350 &opaque_types,
351 &enum_names,
352 &api.enums,
353 &exclude_functions,
354 &bridge_type_aliases_set,
355 &never_skip_cfg_field_names,
356 &mutex_types,
357 ));
358 }
359 }
360
361 for enum_def in &api.enums {
362 if is_tagged_data_enum(enum_def) {
363 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
365 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
366 } else {
367 builder.add_item(&gen_enum_constants(enum_def));
368 }
369 }
370
371 let included_functions: Vec<_> = api
376 .functions
377 .iter()
378 .filter(|f| !exclude_functions.contains(&f.name))
379 .collect();
380 if !included_functions.is_empty() {
381 let facade_class_name = extension_name.to_pascal_case();
382 let mut method_items: Vec<String> = Vec::new();
385 for func in included_functions {
386 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
387 if let Some((param_idx, bridge_cfg)) = bridge_param {
388 let bridge_handle_path = bridge_handle_path(api, bridge_cfg, &core_import);
389 method_items.push(crate::trait_bridge::gen_bridge_function(
390 func,
391 param_idx,
392 bridge_cfg,
393 &mapper,
394 &opaque_types,
395 &core_import,
396 &bridge_handle_path,
397 ));
398 } else if func.is_async {
399 method_items.push(gen_async_function_as_static_method(
400 func,
401 &mapper,
402 &opaque_types,
403 &core_import,
404 &config.trait_bridges,
405 &mutex_types,
406 ));
407 } else {
408 method_items.push(gen_function_as_static_method(
409 func,
410 &mapper,
411 &opaque_types,
412 &core_import,
413 &config.trait_bridges,
414 has_serde,
415 &mutex_types,
416 ));
417 }
418 }
419
420 let methods_joined = method_items
421 .iter()
422 .map(|m| {
423 m.lines()
425 .map(|l| {
426 if l.is_empty() {
427 String::new()
428 } else {
429 format!(" {l}")
430 }
431 })
432 .collect::<Vec<_>>()
433 .join("\n")
434 })
435 .collect::<Vec<_>>()
436 .join("\n\n");
437 let php_api_class_name = format!("{facade_class_name}Api");
440 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
442 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
443 let facade_struct = format!(
444 "#[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}}"
445 );
446 builder.add_item(&facade_struct);
447
448 for bridge_cfg in &config.trait_bridges {
450 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
451 let bridge = crate::trait_bridge::gen_trait_bridge(
452 trait_type,
453 bridge_cfg,
454 &core_import,
455 &config.error_type_name(),
456 &config.error_constructor_expr(),
457 api,
458 );
459 for imp in &bridge.imports {
460 builder.add_import(imp);
461 }
462 builder.add_item(&bridge.code);
463 }
464 }
465 }
466
467 let convertible = alef_codegen::conversions::convertible_types(api);
468 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
469 let input_types = alef_codegen::conversions::input_type_names(api);
470 let enum_names_ref = &mapper.enum_names;
475 let bridge_skip_types: Vec<String> = config
476 .trait_bridges
477 .iter()
478 .filter(|b| !matches!(b.bind_via, alef_core::config::BridgeBinding::OptionsField))
479 .filter_map(|b| b.type_alias.clone())
480 .collect();
481 let trait_bridge_arc_wrapper_field_names: Vec<String> = config
486 .trait_bridges
487 .iter()
488 .filter(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField)
489 .filter_map(|b| b.resolved_options_field().map(String::from))
490 .collect();
491 let mut conv_opaque_types: AHashSet<String> = opaque_types.clone();
496 for bridge in &config.trait_bridges {
497 if let Some(alias) = &bridge.type_alias {
498 conv_opaque_types.insert(alias.clone());
499 }
500 }
501 let php_conv_config = ConversionConfig {
502 cast_large_ints_to_i64: true,
503 enum_string_names: Some(enum_names_ref),
504 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
505 json_as_value: true,
509 include_cfg_metadata: false,
510 option_duration_on_defaults: true,
511 from_binding_skip_types: &bridge_skip_types,
512 never_skip_cfg_field_names: &never_skip_cfg_field_names,
513 opaque_types: Some(&conv_opaque_types),
514 trait_bridge_arc_wrapper_field_names: &trait_bridge_arc_wrapper_field_names,
515 ..Default::default()
516 };
517 let mut enum_tainted: AHashSet<String> = AHashSet::new();
519 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
520 if has_enum_named_field(typ, enum_names_ref) {
521 enum_tainted.insert(typ.name.clone());
522 }
523 }
524 let mut changed = true;
526 while changed {
527 changed = false;
528 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
529 if !enum_tainted.contains(&typ.name)
530 && binding_fields(&typ.fields).any(|f| references_named_type(&f.ty, &enum_tainted))
531 {
532 enum_tainted.insert(typ.name.clone());
533 changed = true;
534 }
535 }
536 }
537 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
538 if input_types.contains(&typ.name)
540 && !enum_tainted.contains(&typ.name)
541 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
542 {
543 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
544 typ,
545 &core_import,
546 &php_conv_config,
547 ));
548 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
549 builder.add_item(&gen_enum_tainted_from_binding_to_core(
556 typ,
557 &core_import,
558 enum_names_ref,
559 &enum_tainted,
560 &php_conv_config,
561 &api.enums,
562 &bridge_type_aliases_set,
563 ));
564 }
565 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
567 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
568 typ,
569 &core_import,
570 &opaque_types,
571 &php_conv_config,
572 ));
573 }
574 }
575
576 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
578 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
579 }
580
581 for error in &api.errors {
583 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
584 }
585
586 if has_serde {
590 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
591 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
592 pub fn max_compression_ratio() -> i64 { 100 }\n\
593 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
594 pub fn max_nesting_depth() -> i64 { 1024 }\n\
595 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
596 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
597 pub fn max_iterations() -> i64 { 10_000_000 }\n\
598 pub fn max_xml_depth() -> i64 { 1024 }\n\
599 pub fn max_table_cells() -> i64 { 100_000 }\n\
600 }";
601 builder.add_item(serde_module);
602 }
603
604 let php_config = config.php.as_ref();
610 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
611
612 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
616 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
617 }
618
619 let mut class_registrations = String::new();
622 for typ in api
623 .types
624 .iter()
625 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
626 {
627 class_registrations.push_str(&crate::template_env::render(
628 "php_class_registration.jinja",
629 context! { class_name => &typ.name },
630 ));
631 }
632 if !api.functions.is_empty() {
634 let facade_class_name = extension_name.to_pascal_case();
635 class_registrations.push_str(&crate::template_env::render(
636 "php_class_registration.jinja",
637 context! { class_name => &format!("{facade_class_name}Api") },
638 ));
639 }
640 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
643 class_registrations.push_str(&crate::template_env::render(
644 "php_class_registration.jinja",
645 context! { class_name => &enum_def.name },
646 ));
647 }
648 builder.add_item(&format!(
649 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
650 ));
651
652 let mut content = builder.build();
653
654 for bridge in &config.trait_bridges {
659 if let Some(field_name) = bridge.resolved_options_field() {
660 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
661 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
662 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
663 let builder_type = format!("{}Builder", options_type);
664 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
665 let bridge_handle_path = bridge_handle_path(api, bridge, &core_import);
666
667 let old_method = format!(
673 " 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 }}"
674 );
675 let new_method = format!(
676 " 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 }}"
677 );
678
679 content = content.replace(&old_method, &new_method);
680 }
681 }
682
683 let php_stubs_dir = config
686 .php
687 .as_ref()
688 .and_then(|p| p.stubs.as_ref())
689 .map(|s| s.output.to_string_lossy().to_string())
690 .unwrap_or_else(|| "packages/php/src/".to_string());
691
692 let php_namespace = php_autoload_namespace(config);
693
694 let mut generated_files = vec![GeneratedFile {
695 path: PathBuf::from(&output_dir).join("lib.rs"),
696 content,
697 generated_header: false,
698 }];
699
700 for bridge_cfg in &config.trait_bridges {
702 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
703 let is_visitor_bridge = bridge_cfg.type_alias.is_some()
705 && bridge_cfg.register_fn.is_none()
706 && bridge_cfg.super_trait.is_none()
707 && trait_type.methods.iter().all(|m| m.has_default_impl);
708
709 if is_visitor_bridge {
710 let interface_content = crate::trait_bridge::gen_visitor_interface(
711 trait_type,
712 bridge_cfg,
713 &php_namespace,
714 &HashMap::new(), );
716 let interface_filename = format!("{}Interface.php", bridge_cfg.trait_name);
717 generated_files.push(GeneratedFile {
718 path: PathBuf::from(&php_stubs_dir).join(&interface_filename),
719 content: interface_content,
720 generated_header: false,
721 });
722 }
723 }
724 }
725
726 Ok(generated_files)
727 }
728
729 fn generate_public_api(
730 &self,
731 api: &ApiSurface,
732 config: &ResolvedCrateConfig,
733 ) -> anyhow::Result<Vec<GeneratedFile>> {
734 let extension_name = config.php_extension_name();
735 let class_name = extension_name.to_pascal_case();
736
737 let mut content = String::new();
739 content.push_str(&crate::template_env::render(
740 "php_file_header.jinja",
741 minijinja::Value::default(),
742 ));
743 content.push_str(&hash::header(CommentStyle::DoubleSlash));
744 content.push_str(&crate::template_env::render(
745 "php_declare_strict_types.jinja",
746 minijinja::Value::default(),
747 ));
748 content.push('\n');
750
751 let namespace = php_autoload_namespace(config);
753
754 content.push_str(&crate::template_env::render(
755 "php_namespace.jinja",
756 context! { namespace => &namespace },
757 ));
758 content.push('\n');
760 content.push_str(&crate::template_env::render(
761 "php_facade_class_declaration.jinja",
762 context! { class_name => &class_name },
763 ));
764
765 let bridge_param_names_pub: ahash::AHashSet<&str> = config
767 .trait_bridges
768 .iter()
769 .filter_map(|b| b.param_name.as_deref())
770 .collect();
771
772 let no_arg_constructor_types: AHashSet<String> = api
777 .types
778 .iter()
779 .filter(|t| t.fields.iter().all(|f| f.optional))
780 .map(|t| t.name.clone())
781 .collect();
782
783 for func in &api.functions {
785 let method_name = func.name.to_lower_camel_case();
790 let return_php_type = php_type(&func.return_type);
791
792 let visible_params: Vec<_> = func
794 .params
795 .iter()
796 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
797 .collect();
798
799 content.push_str(&crate::template_env::render(
801 "php_phpdoc_block_start.jinja",
802 minijinja::Value::default(),
803 ));
804 if func.doc.is_empty() {
805 content.push_str(&crate::template_env::render(
806 "php_phpdoc_text_line.jinja",
807 context! { text => &format!("{}.", method_name) },
808 ));
809 } else {
810 content.push_str(&crate::template_env::render(
811 "php_phpdoc_lines.jinja",
812 context! {
813 doc_lines => func.doc.lines().collect::<Vec<_>>(),
814 indent => " ",
815 },
816 ));
817 }
818 content.push_str(&crate::template_env::render(
819 "php_phpdoc_empty_line.jinja",
820 minijinja::Value::default(),
821 ));
822 for p in &visible_params {
823 let ptype = php_phpdoc_type(&p.ty);
824 let nullable_prefix = if p.optional { "?" } else { "" };
825 content.push_str(&crate::template_env::render(
826 "php_phpdoc_param_line.jinja",
827 context! {
828 nullable_prefix => nullable_prefix,
829 param_type => &ptype,
830 param_name => &p.name,
831 },
832 ));
833 }
834 let return_phpdoc = php_phpdoc_type(&func.return_type);
835 content.push_str(&crate::template_env::render(
836 "php_phpdoc_return_line.jinja",
837 context! { return_type => &return_phpdoc },
838 ));
839 if func.error_type.is_some() {
840 content.push_str(&crate::template_env::render(
841 "php_phpdoc_throws_line.jinja",
842 context! {
843 namespace => namespace.as_str(),
844 class_name => &class_name,
845 },
846 ));
847 }
848 content.push_str(&crate::template_env::render(
849 "php_phpdoc_block_end.jinja",
850 minijinja::Value::default(),
851 ));
852
853 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
864 if let TypeRef::Named(name) = &p.ty {
865 (name.ends_with("Config") || name.as_str() == "config")
866 && no_arg_constructor_types.contains(name.as_str())
867 } else {
868 false
869 }
870 };
871
872 let mut first_optional_idx = None;
873 for (idx, p) in visible_params.iter().enumerate() {
874 if p.optional || is_optional_config_param(p) {
875 first_optional_idx = Some(idx);
876 break;
877 }
878 }
879
880 content.push_str(&crate::template_env::render(
881 "php_method_signature_start.jinja",
882 context! { method_name => &method_name },
883 ));
884
885 let params: Vec<String> = visible_params
886 .iter()
887 .enumerate()
888 .map(|(idx, p)| {
889 let ptype = php_type(&p.ty);
890 let should_be_optional = p.optional
895 || is_optional_config_param(p)
896 || first_optional_idx.is_some_and(|first| idx >= first);
897 if should_be_optional {
898 format!("?{} ${} = null", ptype, p.name)
899 } else {
900 format!("{} ${}", ptype, p.name)
901 }
902 })
903 .collect();
904 content.push_str(¶ms.join(", "));
905 content.push_str(&crate::template_env::render(
906 "php_method_signature_end.jinja",
907 context! { return_type => &return_php_type },
908 ));
909 let ext_method_name = func.name.to_lower_camel_case();
914 let is_void = matches!(&func.return_type, TypeRef::Unit);
915 let call_params = visible_params
923 .iter()
924 .enumerate()
925 .map(|(idx, p)| {
926 let should_be_optional = p.optional
927 || is_optional_config_param(p)
928 || first_optional_idx.is_some_and(|first| idx >= first);
929 if should_be_optional && is_optional_config_param(p) {
930 if let TypeRef::Named(type_name) = &p.ty {
931 return format!("${} ?? new {}()", p.name, type_name);
932 }
933 }
934 format!("${}", p.name)
935 })
936 .collect::<Vec<_>>()
937 .join(", ");
938 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
939 if is_void {
940 content.push_str(&crate::template_env::render(
941 "php_method_call_statement.jinja",
942 context! { call_expr => &call_expr },
943 ));
944 } else {
945 content.push_str(&crate::template_env::render(
946 "php_method_call_return.jinja",
947 context! { call_expr => &call_expr },
948 ));
949 }
950 content.push_str(&crate::template_env::render(
951 "php_method_end.jinja",
952 minijinja::Value::default(),
953 ));
954 }
955
956 content.push_str(&crate::template_env::render(
957 "php_class_end.jinja",
958 minijinja::Value::default(),
959 ));
960
961 let output_dir = config
965 .php
966 .as_ref()
967 .and_then(|p| p.stubs.as_ref())
968 .map(|s| s.output.to_string_lossy().to_string())
969 .unwrap_or_else(|| "packages/php/src/".to_string());
970
971 let mut files: Vec<GeneratedFile> = Vec::new();
972 files.push(GeneratedFile {
973 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
974 content,
975 generated_header: false,
976 });
977
978 for typ in api.types.iter().filter(|t| t.is_opaque && !t.is_trait) {
984 let streaming_adapters: Vec<&alef_core::config::AdapterConfig> = config
985 .adapters
986 .iter()
987 .filter(|a| {
988 matches!(a.pattern, alef_core::config::AdapterPattern::Streaming)
989 && a.owner_type.as_deref() == Some(&typ.name)
990 && !a.skip_languages.iter().any(|l| l == "php")
991 })
992 .collect();
993 let streaming_method_names: AHashSet<String> = streaming_adapters.iter().map(|a| a.name.clone()).collect();
994 let opaque_file = gen_php_opaque_class_file(typ, &namespace, &streaming_adapters, &streaming_method_names);
995 files.push(GeneratedFile {
996 path: PathBuf::from(&output_dir).join(format!("{}.php", typ.name)),
997 content: opaque_file,
998 generated_header: false,
999 });
1000 }
1001
1002 Ok(files)
1003 }
1004
1005 fn generate_type_stubs(
1006 &self,
1007 api: &ApiSurface,
1008 config: &ResolvedCrateConfig,
1009 ) -> anyhow::Result<Vec<GeneratedFile>> {
1010 let extension_name = config.php_extension_name();
1011 let class_name = extension_name.to_pascal_case();
1012
1013 let namespace = php_autoload_namespace(config);
1015
1016 let mut content = String::new();
1021 content.push_str(&crate::template_env::render(
1022 "php_file_header.jinja",
1023 minijinja::Value::default(),
1024 ));
1025 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1026 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
1027 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
1028 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
1029 content.push_str(&crate::template_env::render(
1030 "php_declare_strict_types.jinja",
1031 minijinja::Value::default(),
1032 ));
1033 content.push('\n');
1035 content.push_str(&crate::template_env::render(
1037 "php_namespace_block_begin.jinja",
1038 context! { namespace => &namespace },
1039 ));
1040
1041 content.push_str(&crate::template_env::render(
1043 "php_exception_class_declaration.jinja",
1044 context! { class_name => &class_name },
1045 ));
1046 content.push_str(
1047 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1048 );
1049 content.push_str("}\n\n");
1050
1051 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
1058 if typ.is_opaque || typ.fields.is_empty() {
1059 continue;
1060 }
1061 if !typ.doc.is_empty() {
1062 content.push_str("/**\n");
1063 content.push_str(&crate::template_env::render(
1064 "php_phpdoc_lines.jinja",
1065 context! {
1066 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
1067 indent => "",
1068 },
1069 ));
1070 content.push_str(" */\n");
1071 }
1072 content.push_str(&crate::template_env::render(
1073 "php_record_class_stub_declaration.jinja",
1074 context! { class_name => &typ.name },
1075 ));
1076
1077 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = binding_fields(&typ.fields).collect();
1080 sorted_fields.sort_by_key(|f| f.optional);
1081
1082 let params: Vec<String> = sorted_fields
1087 .iter()
1088 .map(|f| {
1089 let ptype = php_type(&f.ty);
1090 let nullable = if f.optional && !ptype.starts_with('?') {
1091 format!("?{ptype}")
1092 } else {
1093 ptype
1094 };
1095 let default = if f.optional { " = null" } else { "" };
1096 let php_name = to_php_name(&f.name);
1097 let phpdoc_type = php_phpdoc_type(&f.ty);
1098 let var_type = if f.optional && !phpdoc_type.starts_with('?') {
1099 format!("?{phpdoc_type}")
1100 } else {
1101 phpdoc_type
1102 };
1103 let phpdoc = php_property_phpdoc(&var_type, &f.doc, " ");
1104 format!("{phpdoc} public readonly {nullable} ${php_name}{default}",)
1105 })
1106 .collect();
1107 content.push_str(&crate::template_env::render(
1108 "php_constructor_method.jinja",
1109 context! { params => ¶ms.join(",\n") },
1110 ));
1111
1112 let non_excluded_methods: Vec<&alef_core::ir::MethodDef> = typ
1117 .methods
1118 .iter()
1119 .filter(|m| !m.binding_excluded && !m.sanitized)
1120 .collect();
1121 for method in non_excluded_methods {
1122 let method_name = method.name.to_lower_camel_case();
1123 let is_static = method.receiver.is_none();
1124 let return_type = php_type(&method.return_type);
1125 let first_optional_idx = method.params.iter().position(|p| p.optional);
1126 let params: Vec<String> = method
1127 .params
1128 .iter()
1129 .enumerate()
1130 .map(|(idx, p)| {
1131 let ptype = php_type(&p.ty);
1132 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1133 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1134 format!("{nullable}{ptype} ${} = null", p.name)
1135 } else {
1136 format!("{} ${}", ptype, p.name)
1137 }
1138 })
1139 .collect();
1140 let static_kw = if is_static { "static " } else { "" };
1141 let is_void = matches!(&method.return_type, TypeRef::Unit);
1142 let stub_body = if is_void {
1143 "{ }".to_string()
1144 } else {
1145 "{ throw new \\RuntimeException('Not implemented — provided by the native extension.'); }"
1146 .to_string()
1147 };
1148 content.push_str(&format!(
1149 " public {static_kw}function {method_name}({}): {return_type}\n {stub_body}\n",
1150 params.join(", ")
1151 ));
1152 }
1153
1154 content.push_str("}\n\n");
1155 }
1156
1157 for enum_def in &api.enums {
1160 if is_tagged_data_enum(enum_def) {
1161 if !enum_def.doc.is_empty() {
1163 content.push_str("/**\n");
1164 content.push_str(&crate::template_env::render(
1165 "php_phpdoc_lines.jinja",
1166 context! {
1167 doc_lines => enum_def.doc.lines().collect::<Vec<_>>(),
1168 indent => "",
1169 },
1170 ));
1171 content.push_str(" */\n");
1172 }
1173 content.push_str(&crate::template_env::render(
1174 "php_record_class_stub_declaration.jinja",
1175 context! { class_name => &enum_def.name },
1176 ));
1177 content.push_str("}\n\n");
1178 } else {
1179 content.push_str(&crate::template_env::render(
1181 "php_tagged_enum_declaration.jinja",
1182 context! { enum_name => &enum_def.name },
1183 ));
1184 for variant in &enum_def.variants {
1185 let case_name = sanitize_php_enum_case(&variant.name);
1186 content.push_str(&crate::template_env::render(
1187 "php_enum_variant_stub.jinja",
1188 context! {
1189 variant_name => case_name,
1190 value => &variant.name,
1191 },
1192 ));
1193 }
1194 content.push_str("}\n\n");
1195 }
1196 }
1197
1198 if !api.functions.is_empty() {
1203 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1205 .trait_bridges
1206 .iter()
1207 .filter_map(|b| b.param_name.as_deref())
1208 .collect();
1209
1210 content.push_str(&crate::template_env::render(
1211 "php_api_class_declaration.jinja",
1212 context! { class_name => &class_name },
1213 ));
1214 for func in &api.functions {
1215 let return_type = php_type_fq(&func.return_type, &namespace);
1216 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1217 let visible_params: Vec<_> = func
1219 .params
1220 .iter()
1221 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1222 .collect();
1223 let has_array_params = visible_params
1230 .iter()
1231 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1232 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1233 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1234 let first_optional_idx = visible_params.iter().position(|p| p.optional);
1235 if has_array_params || has_array_return {
1236 content.push_str(" /**\n");
1237 for (idx, p) in visible_params.iter().enumerate() {
1238 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1239 let nullable_prefix = if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1240 "?"
1241 } else {
1242 ""
1243 };
1244 content.push_str(&crate::template_env::render(
1245 "php_phpdoc_static_param.jinja",
1246 context! {
1247 nullable_prefix => nullable_prefix,
1248 ptype => &ptype,
1249 param_name => &p.name,
1250 },
1251 ));
1252 }
1253 content.push_str(&crate::template_env::render(
1254 "php_phpdoc_static_return.jinja",
1255 context! { return_phpdoc => &return_phpdoc },
1256 ));
1257 content.push_str(" */\n");
1258 }
1259 let params: Vec<String> = visible_params
1260 .iter()
1261 .enumerate()
1262 .map(|(idx, p)| {
1263 let ptype = php_type_fq(&p.ty, &namespace);
1264 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1265 let nullable_ptype = if ptype.starts_with('?') {
1266 ptype
1267 } else {
1268 format!("?{ptype}")
1269 };
1270 format!("{} ${} = null", nullable_ptype, p.name)
1271 } else {
1272 format!("{} ${}", ptype, p.name)
1273 }
1274 })
1275 .collect();
1276 let stub_method_name = func.name.to_lower_camel_case();
1280 let is_void_stub = return_type == "void";
1281 let stub_body = if is_void_stub {
1282 "{ }".to_string()
1283 } else {
1284 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1285 };
1286 content.push_str(&crate::template_env::render(
1287 "php_static_method_stub.jinja",
1288 context! {
1289 method_name => &stub_method_name,
1290 params => ¶ms.join(", "),
1291 return_type => &return_type,
1292 stub_body => &stub_body,
1293 },
1294 ));
1295 }
1296 content.push_str("}\n\n");
1297 }
1298
1299 content.push_str(&crate::template_env::render(
1301 "php_namespace_block_end.jinja",
1302 minijinja::Value::default(),
1303 ));
1304
1305 let output_dir = config
1307 .php
1308 .as_ref()
1309 .and_then(|p| p.stubs.as_ref())
1310 .map(|s| s.output.to_string_lossy().to_string())
1311 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1312
1313 Ok(vec![GeneratedFile {
1314 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1315 content,
1316 generated_header: false,
1317 }])
1318 }
1319
1320 fn build_config(&self) -> Option<BuildConfig> {
1321 Some(BuildConfig {
1322 tool: "cargo",
1323 crate_suffix: "-php",
1324 build_dep: BuildDependency::None,
1325 post_build: vec![],
1326 })
1327 }
1328}
1329
1330fn bridge_handle_path(api: &ApiSurface, bridge: &alef_core::config::TraitBridgeConfig, core_import: &str) -> String {
1331 let alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
1332 api.types
1333 .iter()
1334 .find(|t| t.name == alias && !t.rust_path.is_empty())
1335 .map(|t| t.rust_path.replace('-', "_"))
1336 .or_else(|| api.excluded_type_paths.get(alias).map(|path| path.replace('-', "_")))
1337 .unwrap_or_else(|| format!("{core_import}::visitor::{alias}"))
1338}
1339
1340fn php_phpdoc_type(ty: &TypeRef) -> String {
1343 match ty {
1344 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1345 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1346 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1347 _ => php_type(ty),
1348 }
1349}
1350
1351fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1353 match ty {
1354 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1355 TypeRef::Map(k, v) => format!(
1356 "array<{}, {}>",
1357 php_phpdoc_type_fq(k, namespace),
1358 php_phpdoc_type_fq(v, namespace)
1359 ),
1360 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1361 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1362 _ => php_type(ty),
1363 }
1364}
1365
1366fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1368 match ty {
1369 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1370 TypeRef::Optional(inner) => {
1371 let inner_type = php_type_fq(inner, namespace);
1372 if inner_type.starts_with('?') {
1373 inner_type
1374 } else {
1375 format!("?{inner_type}")
1376 }
1377 }
1378 _ => php_type(ty),
1379 }
1380}
1381
1382fn gen_php_opaque_class_file(
1389 typ: &alef_core::ir::TypeDef,
1390 namespace: &str,
1391 streaming_adapters: &[&alef_core::config::AdapterConfig],
1392 streaming_method_names: &AHashSet<String>,
1393) -> String {
1394 let mut content = String::new();
1395 content.push_str(&crate::template_env::render(
1396 "php_file_header.jinja",
1397 minijinja::Value::default(),
1398 ));
1399 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1400 content.push_str(&crate::template_env::render(
1401 "php_declare_strict_types.jinja",
1402 minijinja::Value::default(),
1403 ));
1404 content.push('\n');
1406 content.push_str(&crate::template_env::render(
1407 "php_namespace.jinja",
1408 context! { namespace => namespace },
1409 ));
1410 content.push('\n');
1412
1413 if !typ.doc.is_empty() {
1415 content.push_str("/**\n");
1416 content.push_str(&crate::template_env::render(
1417 "php_phpdoc_lines.jinja",
1418 context! {
1419 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
1420 indent => "",
1421 },
1422 ));
1423 content.push_str(" */\n");
1424 }
1425
1426 content.push_str(&format!("final class {}\n{{\n", typ.name));
1427
1428 let mut method_order: Vec<&alef_core::ir::MethodDef> = Vec::new();
1431 method_order.extend(
1432 typ.methods
1433 .iter()
1434 .filter(|m| m.receiver.is_some() && !streaming_method_names.contains(&m.name)),
1435 );
1436 method_order.extend(
1437 typ.methods
1438 .iter()
1439 .filter(|m| m.receiver.is_none() && !streaming_method_names.contains(&m.name)),
1440 );
1441
1442 for method in method_order {
1443 let method_name = method.name.to_lower_camel_case();
1444 let return_type = php_type(&method.return_type);
1445 let is_void = matches!(&method.return_type, TypeRef::Unit);
1446 let is_static = method.receiver.is_none();
1447
1448 let mut doc_lines: Vec<String> = vec![];
1450 let doc_line = method.doc.lines().next().unwrap_or("").trim();
1451 if !doc_line.is_empty() {
1452 doc_lines.push(doc_line.to_string());
1453 }
1454
1455 let mut phpdoc_params: Vec<String> = vec![];
1457 for param in &method.params {
1458 if matches!(¶m.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)) {
1459 let phpdoc_type = php_phpdoc_type(¶m.ty);
1460 phpdoc_params.push(format!("@param {} ${}", phpdoc_type, param.name));
1461 }
1462 }
1463 doc_lines.extend(phpdoc_params);
1464
1465 let needs_return_phpdoc = matches!(&method.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _));
1467 if needs_return_phpdoc {
1468 let phpdoc_type = php_phpdoc_type(&method.return_type);
1469 doc_lines.push(format!("@return {phpdoc_type}"));
1470 }
1471
1472 if !doc_lines.is_empty() {
1474 content.push_str(" /**\n");
1475 for line in doc_lines {
1476 content.push_str(&format!(" * {}\n", line));
1477 }
1478 content.push_str(" */\n");
1479 }
1480
1481 let static_kw = if is_static { "static " } else { "" };
1483 let first_optional_idx = method.params.iter().position(|p| p.optional);
1484 let params: Vec<String> = method
1485 .params
1486 .iter()
1487 .enumerate()
1488 .map(|(idx, p)| {
1489 let ptype = php_type(&p.ty);
1490 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1491 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1492 format!("{nullable}{ptype} ${} = null", p.name)
1493 } else {
1494 format!("{} ${}", ptype, p.name)
1495 }
1496 })
1497 .collect();
1498 content.push_str(&format!(
1499 " public {static_kw}function {method_name}({}): {return_type}\n",
1500 params.join(", ")
1501 ));
1502 let body = if is_void {
1503 " {\n }\n"
1504 } else {
1505 " {\n throw new \\RuntimeException('Not implemented — provided by the native extension.');\n }\n"
1506 };
1507 content.push_str(body);
1508 }
1509
1510 for adapter in streaming_adapters {
1512 let item_type = adapter.item_type.as_deref().unwrap_or("array");
1513 content.push_str(&gen_php_streaming_method_wrapper(adapter, item_type));
1514 content.push('\n');
1515 }
1516
1517 content.push_str("}\n");
1518 content
1519}
1520
1521fn gen_php_streaming_method_wrapper(adapter: &alef_core::config::AdapterConfig, _item_type: &str) -> String {
1527 let method_name = adapter.name.to_lower_camel_case();
1528
1529 let mut params_vec: Vec<String> = Vec::new();
1531
1532 for p in &adapter.params {
1533 let ptype = php_type(&alef_core::ir::TypeRef::Named(p.ty.clone()));
1534 let nullable = if p.optional { "?" } else { "" };
1535 let default = if p.optional { " = null" } else { "" };
1536 params_vec.push(format!("{nullable}{ptype} ${}{default}", p.name));
1537 }
1538
1539 let params_sig = params_vec.join(", ");
1540
1541 format!(
1546 " public function {method_name}({params_sig}): \\Generator\n {{\n \
1547 throw new \\RuntimeException('Not implemented — provided by the native extension.');\n \
1548 }}\n",
1549 method_name = method_name,
1550 )
1551}
1552
1553fn php_type(ty: &TypeRef) -> String {
1555 match ty {
1556 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1557 TypeRef::Primitive(p) => match p {
1558 PrimitiveType::Bool => "bool".to_string(),
1559 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1560 PrimitiveType::U8
1561 | PrimitiveType::U16
1562 | PrimitiveType::U32
1563 | PrimitiveType::U64
1564 | PrimitiveType::I8
1565 | PrimitiveType::I16
1566 | PrimitiveType::I32
1567 | PrimitiveType::I64
1568 | PrimitiveType::Usize
1569 | PrimitiveType::Isize => "int".to_string(),
1570 },
1571 TypeRef::Optional(inner) => {
1572 let inner_type = php_type(inner);
1575 if inner_type.starts_with('?') {
1576 inner_type
1577 } else {
1578 format!("?{inner_type}")
1579 }
1580 }
1581 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1582 TypeRef::Named(name) => name.clone(),
1583 TypeRef::Unit => "void".to_string(),
1584 TypeRef::Duration => "float".to_string(),
1585 }
1586}
1587
1588fn php_property_phpdoc(var_type: &str, doc: &str, indent: &str) -> String {
1597 let doc = doc.trim();
1598 if doc.is_empty() {
1599 return format!("{indent}/** @var {var_type} */\n");
1600 }
1601 let lines: Vec<&str> = doc.lines().collect();
1602 if lines.len() == 1 {
1603 let line = lines[0].trim();
1604 return format!("{indent}/** @var {var_type} {line} */\n");
1605 }
1606 let mut out = format!("{indent}/**\n");
1608 for line in &lines {
1609 let trimmed = line.trim();
1610 if trimmed.is_empty() {
1611 out.push_str(&format!("{indent} *\n"));
1612 } else {
1613 out.push_str(&format!("{indent} * {trimmed}\n"));
1614 }
1615 }
1616 out.push_str(&format!("{indent} *\n"));
1617 out.push_str(&format!("{indent} * @var {var_type}\n"));
1618 out.push_str(&format!("{indent} */\n"));
1619 out
1620}