1mod functions;
2mod helpers;
3pub mod types;
4
5use crate::type_map::PhpMapper;
6use ahash::AHashSet;
7use alef_codegen::builder::RustFileBuilder;
8use alef_codegen::conversions::ConversionConfig;
9use alef_codegen::doc_emission::{self, DocTarget, sanitize_rust_idioms};
10use alef_codegen::generators::RustBindingConfig;
11use alef_codegen::generators::{self, AsyncPattern};
12use alef_codegen::naming::to_php_name;
13use alef_codegen::shared::binding_fields;
14use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
15use alef_core::config::{Language, ResolvedCrateConfig, detect_serde_available, resolve_output_dir};
16use alef_core::hash::{self, CommentStyle};
17use alef_core::ir::ApiSurface;
18use alef_core::ir::{PrimitiveType, TypeRef};
19use heck::{ToLowerCamelCase, ToPascalCase};
20use minijinja::context;
21use std::collections::HashMap;
22use std::path::PathBuf;
23
24use crate::naming::php_autoload_namespace;
25use functions::{gen_async_function_as_static_method, gen_function_as_static_method};
26
27fn sanitize_php_enum_case(name: &str) -> String {
30 if name.eq_ignore_ascii_case("class") {
31 format!("{name}_")
32 } else {
33 name.to_string()
34 }
35}
36use helpers::{gen_enum_tainted_from_binding_to_core, gen_tokio_runtime, has_enum_named_field, references_named_type};
37use types::{
38 gen_enum_constants, gen_flat_data_enum, gen_flat_data_enum_from_impls, gen_flat_data_enum_methods, gen_php_struct,
39 is_tagged_data_enum, is_untagged_data_enum,
40};
41
42pub struct PhpBackend;
43
44impl PhpBackend {
45 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
46 RustBindingConfig {
47 struct_attrs: &["php_class"],
48 field_attrs: &[],
49 struct_derives: &["Clone"],
50 method_block_attr: Some("php_impl"),
51 constructor_attr: "",
52 static_attr: None,
53 function_attr: "#[php_function]",
54 enum_attrs: &[],
55 enum_derives: &[],
56 needs_signature: false,
57 signature_prefix: "",
58 signature_suffix: "",
59 core_import,
60 async_pattern: AsyncPattern::TokioBlockOn,
61 has_serde,
62 type_name_prefix: "",
63 option_duration_on_defaults: true,
64 opaque_type_names: &[],
65 skip_impl_constructor: false,
66 cast_uints_to_i32: false,
67 cast_large_ints_to_f64: false,
68 named_non_opaque_params_by_ref: false,
69 lossy_skip_types: &[],
70 serializable_opaque_type_names: &[],
71 never_skip_cfg_field_names: &[],
72 }
73 }
74}
75
76impl Backend for PhpBackend {
77 fn name(&self) -> &str {
78 "php"
79 }
80
81 fn language(&self) -> Language {
82 Language::Php
83 }
84
85 fn capabilities(&self) -> Capabilities {
86 Capabilities {
87 supports_async: false,
88 supports_classes: true,
89 supports_enums: true,
90 supports_option: true,
91 supports_result: true,
92 ..Capabilities::default()
93 }
94 }
95
96 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
97 let data_enum_names: AHashSet<String> = api
100 .enums
101 .iter()
102 .filter(|e| is_tagged_data_enum(e))
103 .map(|e| e.name.clone())
104 .collect();
105 let untagged_data_enum_names: AHashSet<String> = api
106 .enums
107 .iter()
108 .filter(|e| is_untagged_data_enum(e))
109 .map(|e| e.name.clone())
110 .collect();
111 let enum_names: AHashSet<String> = api
114 .enums
115 .iter()
116 .filter(|e| !is_tagged_data_enum(e) && !is_untagged_data_enum(e))
117 .map(|e| e.name.clone())
118 .collect();
119 let mapper = PhpMapper {
120 enum_names: enum_names.clone(),
121 data_enum_names: data_enum_names.clone(),
122 untagged_data_enum_names: untagged_data_enum_names.clone(),
123 };
124 let core_import = config.core_import_name();
125 let lang_rename_all = config.serde_rename_all_for_language(Language::Php);
126
127 let php_config = config.php.as_ref();
129 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
130 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
131
132 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
133 let has_serde = detect_serde_available(&output_dir);
134
135 let bridge_type_aliases_php: Vec<String> = config
141 .trait_bridges
142 .iter()
143 .filter_map(|b| b.type_alias.clone())
144 .collect();
145 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
146 let mut opaque_names_vec_php: Vec<String> = api
147 .types
148 .iter()
149 .filter(|t| t.is_opaque)
150 .map(|t| t.name.clone())
151 .collect();
152 opaque_names_vec_php.extend(bridge_type_aliases_php);
153
154 let mut cfg = Self::binding_config(&core_import, has_serde);
155 cfg.opaque_type_names = &opaque_names_vec_php;
156 let never_skip_cfg_field_names: Vec<String> = config
157 .trait_bridges
158 .iter()
159 .filter_map(|b| {
160 if b.bind_via == alef_core::config::BridgeBinding::OptionsField {
161 b.resolved_options_field().map(|s| s.to_string())
162 } else {
163 None
164 }
165 })
166 .collect();
167 cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;
168
169 let mut builder = RustFileBuilder::new().with_generated_header();
171 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
172 builder.add_inner_attribute("allow(unsafe_code)");
173 builder.add_inner_attribute("allow(non_snake_case)");
175 builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions, clippy::arc_with_non_send_sync, clippy::collapsible_if, clippy::clone_on_copy, clippy::should_implement_trait, clippy::useless_conversion)");
176 builder.add_import("ext_php_rs::prelude::*");
177
178 if has_serde {
180 builder.add_import("serde_json");
181 }
182
183 for trait_path in generators::collect_trait_imports(api) {
185 builder.add_import(&trait_path);
186 }
187
188 let has_maps = api.types.iter().any(|t| {
190 t.fields
191 .iter()
192 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
193 }) || api
194 .functions
195 .iter()
196 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
197 if has_maps {
198 builder.add_import("std::collections::HashMap");
199 }
200
201 builder.add_item(
206 "#[derive(Debug, Clone, Default)]\n\
207 pub struct PhpBytes(pub Vec<u8>);\n\
208 \n\
209 impl<'a> ext_php_rs::convert::FromZval<'a> for PhpBytes {\n \
210 const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::String;\n \
211 fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {\n \
212 zval.zend_str().map(|zs| PhpBytes(zs.as_bytes().to_vec()))\n \
213 }\n\
214 }\n\
215 \n\
216 impl From<PhpBytes> for Vec<u8> {\n \
217 fn from(b: PhpBytes) -> Self { b.0 }\n\
218 }\n\
219 \n\
220 impl From<Vec<u8>> for PhpBytes {\n \
221 fn from(v: Vec<u8>) -> Self { PhpBytes(v) }\n\
222 }\n",
223 );
224
225 let custom_mods = config.custom_modules.for_language(Language::Php);
227 for module in custom_mods {
228 builder.add_item(&format!("pub mod {module};"));
229 }
230
231 let has_async =
233 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
234
235 if has_async {
236 builder.add_item(&gen_tokio_runtime());
237 }
238
239 let opaque_types: AHashSet<String> = api
241 .types
242 .iter()
243 .filter(|t| t.is_opaque)
244 .map(|t| t.name.clone())
245 .collect();
246 if !opaque_types.is_empty() {
247 builder.add_import("std::sync::Arc");
248 }
249
250 let mutex_types: AHashSet<String> = api
252 .types
253 .iter()
254 .filter(|t| t.is_opaque && alef_codegen::generators::type_needs_mutex(t))
255 .map(|t| t.name.clone())
256 .collect();
257 if !mutex_types.is_empty() {
258 builder.add_import("std::sync::Mutex");
259 }
260
261 let extension_name = config.php_extension_name();
264 let php_namespace = php_autoload_namespace(config);
265
266 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
268
269 let streaming_method_keys: AHashSet<String> = config
274 .adapters
275 .iter()
276 .filter(|a| matches!(a.pattern, alef_core::config::AdapterPattern::Streaming))
277 .filter_map(|a| a.owner_type.as_deref().map(|owner| format!("{owner}.{}", a.name)))
278 .collect();
279
280 for adapter in &config.adapters {
282 match adapter.pattern {
283 alef_core::config::AdapterPattern::Streaming => {
284 let key = alef_adapters::stream_struct_key(adapter);
285 if let Some(struct_code) = adapter_bodies.get(&key) {
286 builder.add_item(struct_code);
287 }
288 }
289 alef_core::config::AdapterPattern::CallbackBridge => {
290 let struct_key = format!("{}.__bridge_struct__", adapter.name);
291 let impl_key = format!("{}.__bridge_impl__", adapter.name);
292 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
293 builder.add_item(struct_code);
294 }
295 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
296 builder.add_item(impl_code);
297 }
298 }
299 _ => {}
300 }
301 }
302
303 for typ in api
304 .types
305 .iter()
306 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
307 {
308 if typ.is_opaque {
309 let ns_escaped = php_namespace.replace('\\', "\\\\");
313 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
314 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
315 let opaque_cfg = RustBindingConfig {
316 struct_attrs: &opaque_attr_arr,
317 ..cfg
318 };
319 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
320 builder.add_item(&types::gen_opaque_struct_methods_with_exclude(
321 typ,
322 &mapper,
323 &opaque_types,
324 &core_import,
325 &adapter_bodies,
326 &mutex_types,
327 &streaming_method_keys,
328 ));
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 if alef_codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&func.name, &config.trait_bridges)
388 {
389 continue;
390 }
391 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
392 if let Some((param_idx, bridge_cfg)) = bridge_param {
393 let bridge_handle_path = bridge_handle_path(api, bridge_cfg, &core_import);
394 method_items.push(crate::trait_bridge::gen_bridge_function(
395 func,
396 param_idx,
397 bridge_cfg,
398 &mapper,
399 &opaque_types,
400 &core_import,
401 &bridge_handle_path,
402 ));
403 } else if func.is_async {
404 method_items.push(gen_async_function_as_static_method(
405 func,
406 &mapper,
407 &opaque_types,
408 &core_import,
409 &config.trait_bridges,
410 &mutex_types,
411 ));
412 } else {
413 method_items.push(gen_function_as_static_method(
414 func,
415 &mapper,
416 &opaque_types,
417 &core_import,
418 &config.trait_bridges,
419 has_serde,
420 &mutex_types,
421 ));
422 }
423 }
424
425 for bridge_cfg in &config.trait_bridges {
427 if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
428 method_items.push(format!(
429 "pub fn {}(backend: &mut ext_php_rs::types::ZendObject) -> ext_php_rs::prelude::PhpResult<()> {{\n \
430 crate::{}(backend)\n}}",
431 register_fn,
432 register_fn
433 ));
434 }
435 if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
436 method_items.push(format!(
437 "pub fn {}(name: String) -> ext_php_rs::prelude::PhpResult<()> {{\n \
438 crate::{}(name)\n}}",
439 unregister_fn, unregister_fn
440 ));
441 }
442 if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
443 method_items.push(format!(
444 "pub fn {}() -> ext_php_rs::prelude::PhpResult<()> {{\n \
445 crate::{}()\n}}",
446 clear_fn, clear_fn
447 ));
448 }
449 }
450
451 let methods_joined = method_items
452 .iter()
453 .map(|m| {
454 m.lines()
456 .map(|l| {
457 if l.is_empty() {
458 String::new()
459 } else {
460 format!(" {l}")
461 }
462 })
463 .collect::<Vec<_>>()
464 .join("\n")
465 })
466 .collect::<Vec<_>>()
467 .join("\n\n");
468 let php_api_class_name = format!("{facade_class_name}Api");
471 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
473 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
474 let facade_struct = format!(
475 "#[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}}"
476 );
477 builder.add_item(&facade_struct);
478
479 for bridge_cfg in &config.trait_bridges {
481 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
482 let bridge = crate::trait_bridge::gen_trait_bridge(
483 trait_type,
484 bridge_cfg,
485 &core_import,
486 &config.error_type_name(),
487 &config.error_constructor_expr(),
488 api,
489 );
490 for imp in &bridge.imports {
491 builder.add_import(imp);
492 }
493 builder.add_item(&bridge.code);
494 }
495 }
496 }
497
498 let convertible = alef_codegen::conversions::convertible_types(api);
499 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
500 let input_types = alef_codegen::conversions::input_type_names(api);
501 let enum_names_ref = &mapper.enum_names;
506 let bridge_skip_types: Vec<String> = config
507 .trait_bridges
508 .iter()
509 .filter(|b| !matches!(b.bind_via, alef_core::config::BridgeBinding::OptionsField))
510 .filter_map(|b| b.type_alias.clone())
511 .collect();
512 let trait_bridge_arc_wrapper_field_names: Vec<String> = config
517 .trait_bridges
518 .iter()
519 .filter(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField)
520 .filter_map(|b| b.resolved_options_field().map(String::from))
521 .collect();
522 let mut conv_opaque_types: AHashSet<String> = opaque_types.clone();
527 for bridge in &config.trait_bridges {
528 if let Some(alias) = &bridge.type_alias {
529 conv_opaque_types.insert(alias.clone());
530 }
531 }
532 let php_conv_config = ConversionConfig {
533 cast_large_ints_to_i64: true,
534 enum_string_names: Some(enum_names_ref),
535 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
536 json_as_value: true,
540 include_cfg_metadata: false,
541 option_duration_on_defaults: true,
542 from_binding_skip_types: &bridge_skip_types,
543 never_skip_cfg_field_names: &never_skip_cfg_field_names,
544 opaque_types: Some(&conv_opaque_types),
545 trait_bridge_arc_wrapper_field_names: &trait_bridge_arc_wrapper_field_names,
546 ..Default::default()
547 };
548 let mut enum_tainted: AHashSet<String> = AHashSet::new();
550 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
551 if has_enum_named_field(typ, enum_names_ref) {
552 enum_tainted.insert(typ.name.clone());
553 }
554 }
555 let mut changed = true;
557 while changed {
558 changed = false;
559 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
560 if !enum_tainted.contains(&typ.name)
561 && binding_fields(&typ.fields).any(|f| references_named_type(&f.ty, &enum_tainted))
562 {
563 enum_tainted.insert(typ.name.clone());
564 changed = true;
565 }
566 }
567 }
568 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
569 if input_types.contains(&typ.name)
571 && !enum_tainted.contains(&typ.name)
572 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
573 {
574 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
575 typ,
576 &core_import,
577 &php_conv_config,
578 ));
579 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
580 builder.add_item(&gen_enum_tainted_from_binding_to_core(
587 typ,
588 &core_import,
589 enum_names_ref,
590 &enum_tainted,
591 &php_conv_config,
592 &api.enums,
593 &bridge_type_aliases_set,
594 ));
595 }
596 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
598 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
599 typ,
600 &core_import,
601 &opaque_types,
602 &php_conv_config,
603 ));
604 }
605 }
606
607 let mut emitted_binding_to_core: AHashSet<String> = api
617 .types
618 .iter()
619 .filter(|typ| !typ.is_trait && input_types.contains(&typ.name))
620 .filter(|typ| {
621 (enum_tainted.contains(&typ.name))
622 || alef_codegen::conversions::can_generate_conversion(typ, &convertible)
623 })
624 .map(|typ| typ.name.clone())
625 .collect();
626 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
627 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
628 for variant in &enum_def.variants {
631 for field in &variant.fields {
632 if let TypeRef::Named(type_name) = &field.ty {
633 if let Some(typ) = api.types.iter().find(|t| &t.name == type_name) {
634 if emitted_binding_to_core.contains(&typ.name) {
635 continue;
636 }
637 if enum_tainted.contains(&typ.name) {
638 builder.add_item(&gen_enum_tainted_from_binding_to_core(
639 typ,
640 &core_import,
641 enum_names_ref,
642 &enum_tainted,
643 &php_conv_config,
644 &api.enums,
645 &bridge_type_aliases_set,
646 ));
647 emitted_binding_to_core.insert(typ.name.clone());
648 } else if alef_codegen::conversions::can_generate_conversion(typ, &convertible) {
649 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
650 typ,
651 &core_import,
652 &php_conv_config,
653 ));
654 emitted_binding_to_core.insert(typ.name.clone());
655 }
656 }
657 }
658 }
659 }
660 }
661
662 for typ in api.types.iter().filter(|t| !t.is_trait) {
666 if !emitted_binding_to_core.contains(&typ.name) {
667 if enum_tainted.contains(&typ.name) {
668 builder.add_item(&gen_enum_tainted_from_binding_to_core(
669 typ,
670 &core_import,
671 enum_names_ref,
672 &enum_tainted,
673 &php_conv_config,
674 &api.enums,
675 &bridge_type_aliases_set,
676 ));
677 emitted_binding_to_core.insert(typ.name.clone());
678 } else if alef_codegen::conversions::can_generate_conversion(typ, &convertible) {
679 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
680 typ,
681 &core_import,
682 &php_conv_config,
683 ));
684 emitted_binding_to_core.insert(typ.name.clone());
685 }
686 }
687 }
688
689 for error in &api.errors {
691 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
692 let methods_impl = alef_codegen::error_gen::gen_php_error_methods_impl(error, &core_import);
694 if !methods_impl.is_empty() {
695 builder.add_item(&methods_impl);
696 }
697 }
698
699 if has_serde {
703 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
704 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
705 pub fn max_compression_ratio() -> i64 { 100 }\n\
706 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
707 pub fn max_nesting_depth() -> i64 { 1024 }\n\
708 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
709 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
710 pub fn max_iterations() -> i64 { 10_000_000 }\n\
711 pub fn max_xml_depth() -> i64 { 1024 }\n\
712 pub fn max_table_cells() -> i64 { 100_000 }\n\
713 }";
714 builder.add_item(serde_module);
715 }
716
717 let php_config = config.php.as_ref();
723 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
724
725 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
729 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
730 }
731
732 let mut class_registrations = String::new();
735 for typ in api
736 .types
737 .iter()
738 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
739 {
740 class_registrations.push_str(&crate::template_env::render(
741 "php_class_registration.jinja",
742 context! { class_name => &typ.name },
743 ));
744 }
745 if !api.functions.is_empty() {
747 let facade_class_name = extension_name.to_pascal_case();
748 class_registrations.push_str(&crate::template_env::render(
749 "php_class_registration.jinja",
750 context! { class_name => &format!("{facade_class_name}Api") },
751 ));
752 }
753 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
756 class_registrations.push_str(&crate::template_env::render(
757 "php_class_registration.jinja",
758 context! { class_name => &enum_def.name },
759 ));
760 }
761 for error in api.errors.iter().filter(|e| !e.methods.is_empty()) {
763 let info_class = format!("{}Info", error.name);
764 class_registrations.push_str(&crate::template_env::render(
765 "php_class_registration.jinja",
766 context! { class_name => &info_class },
767 ));
768 }
769 builder.add_item(&format!(
770 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
771 ));
772
773 let mut content = builder.build();
774
775 for bridge in &config.trait_bridges {
780 if let Some(field_name) = bridge.resolved_options_field() {
781 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
782 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
783 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
784 let builder_type = format!("{}Builder", options_type);
785 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
786 let bridge_handle_path = bridge_handle_path(api, bridge, &core_import);
787
788 let old_method = format!(
794 " 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 }}"
795 );
796 let new_method = format!(
797 " 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 }}"
798 );
799
800 content = content.replace(&old_method, &new_method);
801 }
802 }
803
804 let php_stubs_dir = config
807 .php
808 .as_ref()
809 .and_then(|p| p.stubs.as_ref())
810 .map(|s| s.output.to_string_lossy().to_string())
811 .unwrap_or_else(|| "packages/php/src/".to_string());
812
813 let php_namespace = php_autoload_namespace(config);
814
815 let mut generated_files = vec![GeneratedFile {
816 path: PathBuf::from(&output_dir).join("lib.rs"),
817 content,
818 generated_header: false,
819 }];
820
821 for bridge_cfg in &config.trait_bridges {
823 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
824 let is_visitor_bridge = bridge_cfg.type_alias.is_some()
826 && bridge_cfg.register_fn.is_none()
827 && bridge_cfg.super_trait.is_none()
828 && trait_type.methods.iter().all(|m| m.has_default_impl);
829
830 if is_visitor_bridge {
831 let interface_content = crate::trait_bridge::gen_visitor_interface(
832 trait_type,
833 bridge_cfg,
834 &php_namespace,
835 &HashMap::new(), );
837 let interface_filename = format!("{}Interface.php", bridge_cfg.trait_name);
838 generated_files.push(GeneratedFile {
839 path: PathBuf::from(&php_stubs_dir).join(&interface_filename),
840 content: interface_content,
841 generated_header: false,
842 });
843 }
844 }
845 }
846
847 Ok(generated_files)
848 }
849
850 fn generate_public_api(
851 &self,
852 api: &ApiSurface,
853 config: &ResolvedCrateConfig,
854 ) -> anyhow::Result<Vec<GeneratedFile>> {
855 let escape_phpdoc_line = |s: &str| s.replace("*/", "* /");
857
858 let extension_name = config.php_extension_name();
859 let class_name = extension_name.to_pascal_case();
860
861 let mut content = String::new();
863 content.push_str(&crate::template_env::render(
864 "php_file_header.jinja",
865 minijinja::Value::default(),
866 ));
867 content.push_str(&hash::header(CommentStyle::DoubleSlash));
868 content.push_str(&crate::template_env::render(
869 "php_declare_strict_types.jinja",
870 minijinja::Value::default(),
871 ));
872 content.push('\n');
874
875 let namespace = php_autoload_namespace(config);
877
878 content.push_str(&crate::template_env::render(
879 "php_namespace.jinja",
880 context! { namespace => &namespace },
881 ));
882 content.push('\n');
884 content.push_str(&crate::template_env::render(
885 "php_facade_class_declaration.jinja",
886 context! { class_name => &class_name },
887 ));
888
889 let bridge_param_names_pub: ahash::AHashSet<&str> = config
891 .trait_bridges
892 .iter()
893 .filter_map(|b| b.param_name.as_deref())
894 .collect();
895
896 let no_arg_constructor_types: AHashSet<String> = api
901 .types
902 .iter()
903 .filter(|t| t.fields.iter().all(|f| f.optional))
904 .map(|t| t.name.clone())
905 .collect();
906
907 for func in &api.functions {
909 if alef_codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&func.name, &config.trait_bridges) {
913 continue;
914 }
915 let method_name = func.name.to_lower_camel_case();
920 let return_php_type = php_type(&func.return_type);
921
922 let visible_params: Vec<_> = func
924 .params
925 .iter()
926 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
927 .collect();
928
929 content.push_str(&crate::template_env::render(
931 "php_phpdoc_block_start.jinja",
932 minijinja::Value::default(),
933 ));
934 if func.doc.is_empty() {
935 content.push_str(&crate::template_env::render(
936 "php_phpdoc_text_line.jinja",
937 context! { text => &format!("{}.", method_name) },
938 ));
939 } else {
940 let sections = doc_emission::parse_rustdoc_sections(&func.doc);
942 for line in sections.summary.lines() {
944 content.push_str(" * ");
945 content.push_str(&escape_phpdoc_line(line));
946 content.push('\n');
947 }
948 }
951 content.push_str(&crate::template_env::render(
952 "php_phpdoc_empty_line.jinja",
953 minijinja::Value::default(),
954 ));
955 for p in &visible_params {
956 let ptype = php_phpdoc_type(&p.ty);
957 let nullable_prefix = if p.optional { "?" } else { "" };
958 content.push_str(&crate::template_env::render(
959 "php_phpdoc_param_line.jinja",
960 context! {
961 nullable_prefix => nullable_prefix,
962 param_type => &ptype,
963 param_name => &p.name,
964 },
965 ));
966 }
967 let return_phpdoc = php_phpdoc_type(&func.return_type);
968 content.push_str(&crate::template_env::render(
969 "php_phpdoc_return_line.jinja",
970 context! { return_type => &return_phpdoc },
971 ));
972 if func.error_type.is_some() {
973 content.push_str(&crate::template_env::render(
974 "php_phpdoc_throws_line.jinja",
975 context! {
976 namespace => namespace.as_str(),
977 class_name => &class_name,
978 },
979 ));
980 }
981 content.push_str(&crate::template_env::render(
982 "php_phpdoc_block_end.jinja",
983 minijinja::Value::default(),
984 ));
985
986 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
997 if let TypeRef::Named(name) = &p.ty {
998 (name.ends_with("Config") || name.as_str() == "config")
999 && no_arg_constructor_types.contains(name.as_str())
1000 } else {
1001 false
1002 }
1003 };
1004
1005 let mut first_optional_idx = None;
1006 for (idx, p) in visible_params.iter().enumerate() {
1007 if p.optional || is_optional_config_param(p) {
1008 first_optional_idx = Some(idx);
1009 break;
1010 }
1011 }
1012
1013 content.push_str(&crate::template_env::render(
1014 "php_method_signature_start.jinja",
1015 context! { method_name => &method_name },
1016 ));
1017
1018 let params: Vec<String> = visible_params
1019 .iter()
1020 .enumerate()
1021 .map(|(idx, p)| {
1022 let ptype = php_type(&p.ty);
1023 let should_be_optional = p.optional
1028 || is_optional_config_param(p)
1029 || first_optional_idx.is_some_and(|first| idx >= first);
1030 if should_be_optional {
1031 format!("?{} ${} = null", ptype, p.name)
1032 } else {
1033 format!("{} ${}", ptype, p.name)
1034 }
1035 })
1036 .collect();
1037 content.push_str(¶ms.join(", "));
1038 content.push_str(&crate::template_env::render(
1039 "php_method_signature_end.jinja",
1040 context! { return_type => &return_php_type },
1041 ));
1042 let ext_method_name = func.name.to_lower_camel_case();
1047 let is_void = matches!(&func.return_type, TypeRef::Unit);
1048 let call_params = visible_params
1056 .iter()
1057 .enumerate()
1058 .map(|(idx, p)| {
1059 let should_be_optional = p.optional
1060 || is_optional_config_param(p)
1061 || first_optional_idx.is_some_and(|first| idx >= first);
1062 if should_be_optional && is_optional_config_param(p) {
1063 if let TypeRef::Named(type_name) = &p.ty {
1064 return format!("${} ?? new {}()", p.name, type_name);
1065 }
1066 }
1067 format!("${}", p.name)
1068 })
1069 .collect::<Vec<_>>()
1070 .join(", ");
1071 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
1072 if is_void {
1073 content.push_str(&crate::template_env::render(
1074 "php_method_call_statement.jinja",
1075 context! { call_expr => &call_expr },
1076 ));
1077 } else {
1078 content.push_str(&crate::template_env::render(
1079 "php_method_call_return.jinja",
1080 context! { call_expr => &call_expr },
1081 ));
1082 }
1083 content.push_str(&crate::template_env::render(
1084 "php_method_end.jinja",
1085 minijinja::Value::default(),
1086 ));
1087 }
1088
1089 for bridge_cfg in &config.trait_bridges {
1091 if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
1092 let method_name = register_fn.to_lower_camel_case();
1093 content.push_str(&crate::template_env::render(
1094 "php_phpdoc_block_start.jinja",
1095 minijinja::Value::default(),
1096 ));
1097 content.push_str(&crate::template_env::render(
1098 "php_phpdoc_text_line.jinja",
1099 context! { text => &format!("{}.", method_name) },
1100 ));
1101 content.push_str(&crate::template_env::render(
1102 "php_phpdoc_empty_line.jinja",
1103 minijinja::Value::default(),
1104 ));
1105 let interface_name = &bridge_cfg.trait_name;
1106 content.push_str(&crate::template_env::render(
1107 "php_phpdoc_param_line.jinja",
1108 context! {
1109 nullable_prefix => "",
1110 param_type => interface_name,
1111 param_name => "backend",
1112 },
1113 ));
1114 content.push_str(&crate::template_env::render(
1115 "php_phpdoc_return_line.jinja",
1116 context! { return_type => "void" },
1117 ));
1118 content.push_str(&crate::template_env::render(
1119 "php_phpdoc_block_end.jinja",
1120 minijinja::Value::default(),
1121 ));
1122 content.push_str(&crate::template_env::render(
1123 "php_method_signature_start.jinja",
1124 context! { method_name => &method_name },
1125 ));
1126 content.push_str(&format!("{} $backend = null) : void\n {{\n", interface_name));
1127 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}($backend)");
1128 content.push_str(&crate::template_env::render(
1129 "php_method_call_statement.jinja",
1130 context! { call_expr => &call_expr },
1131 ));
1132 content.push_str(&crate::template_env::render(
1133 "php_method_end.jinja",
1134 minijinja::Value::default(),
1135 ));
1136 }
1137 if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
1138 let method_name = unregister_fn.to_lower_camel_case();
1139 content.push_str(&crate::template_env::render(
1140 "php_phpdoc_block_start.jinja",
1141 minijinja::Value::default(),
1142 ));
1143 content.push_str(&crate::template_env::render(
1144 "php_phpdoc_text_line.jinja",
1145 context! { text => &format!("{}.", method_name) },
1146 ));
1147 content.push_str(&crate::template_env::render(
1148 "php_phpdoc_empty_line.jinja",
1149 minijinja::Value::default(),
1150 ));
1151 content.push_str(&crate::template_env::render(
1152 "php_phpdoc_param_line.jinja",
1153 context! {
1154 nullable_prefix => "",
1155 param_type => "string",
1156 param_name => "name",
1157 },
1158 ));
1159 content.push_str(&crate::template_env::render(
1160 "php_phpdoc_return_line.jinja",
1161 context! { return_type => "void" },
1162 ));
1163 content.push_str(&crate::template_env::render(
1164 "php_phpdoc_block_end.jinja",
1165 minijinja::Value::default(),
1166 ));
1167 content.push_str(&crate::template_env::render(
1168 "php_method_signature_start.jinja",
1169 context! { method_name => &method_name },
1170 ));
1171 content.push_str("string $name) : void\n {\n");
1172 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}($name)");
1173 content.push_str(&crate::template_env::render(
1174 "php_method_call_statement.jinja",
1175 context! { call_expr => &call_expr },
1176 ));
1177 content.push_str(&crate::template_env::render(
1178 "php_method_end.jinja",
1179 minijinja::Value::default(),
1180 ));
1181 }
1182 if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
1183 let method_name = clear_fn.to_lower_camel_case();
1184 content.push_str(&crate::template_env::render(
1185 "php_phpdoc_block_start.jinja",
1186 minijinja::Value::default(),
1187 ));
1188 content.push_str(&crate::template_env::render(
1189 "php_phpdoc_text_line.jinja",
1190 context! { text => &format!("{}.", method_name) },
1191 ));
1192 content.push_str(&crate::template_env::render(
1193 "php_phpdoc_empty_line.jinja",
1194 minijinja::Value::default(),
1195 ));
1196 content.push_str(&crate::template_env::render(
1197 "php_phpdoc_return_line.jinja",
1198 context! { return_type => "void" },
1199 ));
1200 content.push_str(&crate::template_env::render(
1201 "php_phpdoc_block_end.jinja",
1202 minijinja::Value::default(),
1203 ));
1204 content.push_str(&crate::template_env::render(
1205 "php_method_signature_start.jinja",
1206 context! { method_name => &method_name },
1207 ));
1208 content.push_str(") : void\n {\n");
1209 let call_expr = format!("\\{namespace}\\{class_name}Api::{method_name}()");
1210 content.push_str(&crate::template_env::render(
1211 "php_method_call_statement.jinja",
1212 context! { call_expr => &call_expr },
1213 ));
1214 content.push_str(&crate::template_env::render(
1215 "php_method_end.jinja",
1216 minijinja::Value::default(),
1217 ));
1218 }
1219 }
1220
1221 content.push_str(&crate::template_env::render(
1222 "php_class_end.jinja",
1223 minijinja::Value::default(),
1224 ));
1225
1226 let output_dir = config
1230 .php
1231 .as_ref()
1232 .and_then(|p| p.stubs.as_ref())
1233 .map(|s| s.output.to_string_lossy().to_string())
1234 .unwrap_or_else(|| "packages/php/src/".to_string());
1235
1236 let mut files: Vec<GeneratedFile> = Vec::new();
1237 files.push(GeneratedFile {
1238 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
1239 content,
1240 generated_header: false,
1241 });
1242
1243 for typ in api.types.iter().filter(|t| t.is_opaque && !t.is_trait) {
1249 let streaming_adapters: Vec<&alef_core::config::AdapterConfig> = config
1250 .adapters
1251 .iter()
1252 .filter(|a| {
1253 matches!(a.pattern, alef_core::config::AdapterPattern::Streaming)
1254 && a.owner_type.as_deref() == Some(&typ.name)
1255 && !a.skip_languages.iter().any(|l| l == "php")
1256 })
1257 .collect();
1258 let streaming_method_names: AHashSet<String> = streaming_adapters.iter().map(|a| a.name.clone()).collect();
1259 let opaque_file = gen_php_opaque_class_file(typ, &namespace, &streaming_adapters, &streaming_method_names);
1260 files.push(GeneratedFile {
1261 path: PathBuf::from(&output_dir).join(format!("{}.php", typ.name)),
1262 content: opaque_file,
1263 generated_header: false,
1264 });
1265 }
1266
1267 Ok(files)
1268 }
1269
1270 fn generate_type_stubs(
1271 &self,
1272 api: &ApiSurface,
1273 config: &ResolvedCrateConfig,
1274 ) -> anyhow::Result<Vec<GeneratedFile>> {
1275 let extension_name = config.php_extension_name();
1276 let class_name = extension_name.to_pascal_case();
1277
1278 let namespace = php_autoload_namespace(config);
1280
1281 let mut content = String::new();
1286 content.push_str(&crate::template_env::render(
1287 "php_file_header.jinja",
1288 minijinja::Value::default(),
1289 ));
1290 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1291 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
1292 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
1293 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
1294 content.push_str(&crate::template_env::render(
1295 "php_declare_strict_types.jinja",
1296 minijinja::Value::default(),
1297 ));
1298 content.push('\n');
1300 content.push_str(&crate::template_env::render(
1302 "php_namespace_block_begin.jinja",
1303 context! { namespace => &namespace },
1304 ));
1305
1306 content.push_str(&crate::template_env::render(
1308 "php_exception_class_declaration.jinja",
1309 context! { class_name => &class_name },
1310 ));
1311 content.push_str(
1312 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1313 );
1314 let has_status_code = api
1317 .errors
1318 .iter()
1319 .any(|e| e.methods.iter().any(|m| m.name == "status_code"));
1320 let has_is_transient = api
1321 .errors
1322 .iter()
1323 .any(|e| e.methods.iter().any(|m| m.name == "is_transient"));
1324 let has_error_type = api
1325 .errors
1326 .iter()
1327 .any(|e| e.methods.iter().any(|m| m.name == "error_type"));
1328 if has_status_code {
1329 content.push_str(
1330 " /** HTTP status code for this error (0 means no associated status). */\n \
1331 public function statusCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
1332 );
1333 }
1334 if has_is_transient {
1335 content.push_str(
1336 " /** Returns true if the error is transient and a retry may succeed. */\n \
1337 public function isTransient(): bool { throw new \\RuntimeException('Not implemented.'); }\n",
1338 );
1339 }
1340 if has_error_type {
1341 content.push_str(
1342 " /** Machine-readable error category string for matching and logging. */\n \
1343 public function errorType(): string { throw new \\RuntimeException('Not implemented.'); }\n",
1344 );
1345 }
1346 content.push_str("}\n\n");
1347
1348 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
1355 if typ.is_opaque || typ.fields.is_empty() {
1356 continue;
1357 }
1358 if !typ.doc.is_empty() {
1359 content.push_str("/**\n");
1360 let sanitized = sanitize_rust_idioms(&typ.doc, DocTarget::PhpDoc);
1361 content.push_str(&crate::template_env::render(
1362 "php_phpdoc_lines.jinja",
1363 context! {
1364 doc_lines => sanitized.lines().collect::<Vec<_>>(),
1365 indent => "",
1366 },
1367 ));
1368 content.push_str(" */\n");
1369 }
1370 content.push_str(&crate::template_env::render(
1371 "php_record_class_stub_declaration.jinja",
1372 context! { class_name => &typ.name },
1373 ));
1374
1375 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = binding_fields(&typ.fields).collect();
1378 sorted_fields.sort_by_key(|f| f.optional);
1379
1380 let params: Vec<String> = sorted_fields
1385 .iter()
1386 .map(|f| {
1387 let ptype = php_type(&f.ty);
1388 let nullable = if f.optional && !ptype.starts_with('?') {
1389 format!("?{ptype}")
1390 } else {
1391 ptype
1392 };
1393 let default = if f.optional { " = null" } else { "" };
1394 let php_name = to_php_name(&f.name);
1395 let phpdoc_type = php_phpdoc_type(&f.ty);
1396 let var_type = if f.optional && !phpdoc_type.starts_with('?') {
1397 format!("?{phpdoc_type}")
1398 } else {
1399 phpdoc_type
1400 };
1401 let phpdoc = php_property_phpdoc(&var_type, &f.doc, " ");
1402 format!("{phpdoc} public readonly {nullable} ${php_name}{default}",)
1403 })
1404 .collect();
1405 content.push_str(&crate::template_env::render(
1406 "php_constructor_method.jinja",
1407 context! { params => ¶ms.join(",\n") },
1408 ));
1409
1410 let non_excluded_methods: Vec<&alef_core::ir::MethodDef> = typ
1415 .methods
1416 .iter()
1417 .filter(|m| !m.binding_excluded && !m.sanitized)
1418 .collect();
1419 for method in non_excluded_methods {
1420 let method_name = method.name.to_lower_camel_case();
1421 let is_static = method.receiver.is_none();
1422 let return_type = php_type(&method.return_type);
1423 let first_optional_idx = method.params.iter().position(|p| p.optional);
1424 let params: Vec<String> = method
1425 .params
1426 .iter()
1427 .enumerate()
1428 .map(|(idx, p)| {
1429 let ptype = php_type(&p.ty);
1430 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1431 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1432 format!("{nullable}{ptype} ${} = null", p.name)
1433 } else {
1434 format!("{} ${}", ptype, p.name)
1435 }
1436 })
1437 .collect();
1438 let static_kw = if is_static { "static " } else { "" };
1439 let is_void = matches!(&method.return_type, TypeRef::Unit);
1440 let stub_body = if is_void {
1441 "{ }".to_string()
1442 } else {
1443 "{ throw new \\RuntimeException('Not implemented — provided by the native extension.'); }"
1444 .to_string()
1445 };
1446 content.push_str(&format!(
1447 " public {static_kw}function {method_name}({}): {return_type}\n {stub_body}\n",
1448 params.join(", ")
1449 ));
1450 }
1451
1452 content.push_str("}\n\n");
1453 }
1454
1455 for enum_def in &api.enums {
1458 if is_tagged_data_enum(enum_def) {
1459 if !enum_def.doc.is_empty() {
1461 content.push_str("/**\n");
1462 let sanitized = sanitize_rust_idioms(&enum_def.doc, DocTarget::PhpDoc);
1463 content.push_str(&crate::template_env::render(
1464 "php_phpdoc_lines.jinja",
1465 context! {
1466 doc_lines => sanitized.lines().collect::<Vec<_>>(),
1467 indent => "",
1468 },
1469 ));
1470 content.push_str(" */\n");
1471 }
1472 content.push_str(&crate::template_env::render(
1473 "php_record_class_stub_declaration.jinja",
1474 context! { class_name => &enum_def.name },
1475 ));
1476 content.push_str("}\n\n");
1477 } else {
1478 content.push_str(&crate::template_env::render(
1480 "php_tagged_enum_declaration.jinja",
1481 context! { enum_name => &enum_def.name },
1482 ));
1483 for variant in &enum_def.variants {
1484 let case_name = sanitize_php_enum_case(&variant.name);
1485 content.push_str(&crate::template_env::render(
1486 "php_enum_variant_stub.jinja",
1487 context! {
1488 variant_name => case_name,
1489 value => &variant.name,
1490 },
1491 ));
1492 }
1493 content.push_str("}\n\n");
1494 }
1495 }
1496
1497 if !api.functions.is_empty() {
1502 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1504 .trait_bridges
1505 .iter()
1506 .filter_map(|b| b.param_name.as_deref())
1507 .collect();
1508
1509 content.push_str(&crate::template_env::render(
1510 "php_api_class_declaration.jinja",
1511 context! { class_name => &class_name },
1512 ));
1513 for func in &api.functions {
1514 let return_type = php_type_fq(&func.return_type, &namespace);
1515 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1516 let visible_params: Vec<_> = func
1518 .params
1519 .iter()
1520 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1521 .collect();
1522 let has_array_params = visible_params
1529 .iter()
1530 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1531 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1532 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1533 let first_optional_idx = visible_params.iter().position(|p| p.optional);
1534 if has_array_params || has_array_return {
1535 content.push_str(" /**\n");
1536 for (idx, p) in visible_params.iter().enumerate() {
1537 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1538 let nullable_prefix = if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1539 "?"
1540 } else {
1541 ""
1542 };
1543 content.push_str(&crate::template_env::render(
1544 "php_phpdoc_static_param.jinja",
1545 context! {
1546 nullable_prefix => nullable_prefix,
1547 ptype => &ptype,
1548 param_name => &p.name,
1549 },
1550 ));
1551 }
1552 content.push_str(&crate::template_env::render(
1553 "php_phpdoc_static_return.jinja",
1554 context! { return_phpdoc => &return_phpdoc },
1555 ));
1556 content.push_str(" */\n");
1557 }
1558 let params: Vec<String> = visible_params
1559 .iter()
1560 .enumerate()
1561 .map(|(idx, p)| {
1562 let ptype = php_type_fq(&p.ty, &namespace);
1563 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1564 let nullable_ptype = if ptype.starts_with('?') {
1565 ptype
1566 } else {
1567 format!("?{ptype}")
1568 };
1569 format!("{} ${} = null", nullable_ptype, p.name)
1570 } else {
1571 format!("{} ${}", ptype, p.name)
1572 }
1573 })
1574 .collect();
1575 let stub_method_name = func.name.to_lower_camel_case();
1579 let is_void_stub = return_type == "void";
1580 let stub_body = if is_void_stub {
1581 "{ }".to_string()
1582 } else {
1583 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1584 };
1585 content.push_str(&crate::template_env::render(
1586 "php_static_method_stub.jinja",
1587 context! {
1588 method_name => &stub_method_name,
1589 params => ¶ms.join(", "),
1590 return_type => &return_type,
1591 stub_body => &stub_body,
1592 },
1593 ));
1594 }
1595 content.push_str("}\n\n");
1596 }
1597
1598 content.push_str(&crate::template_env::render(
1600 "php_namespace_block_end.jinja",
1601 minijinja::Value::default(),
1602 ));
1603
1604 let output_dir = config
1606 .php
1607 .as_ref()
1608 .and_then(|p| p.stubs.as_ref())
1609 .map(|s| s.output.to_string_lossy().to_string())
1610 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1611
1612 Ok(vec![GeneratedFile {
1613 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1614 content,
1615 generated_header: false,
1616 }])
1617 }
1618
1619 fn build_config(&self) -> Option<BuildConfig> {
1620 Some(BuildConfig {
1621 tool: "cargo",
1622 crate_suffix: "-php",
1623 build_dep: BuildDependency::None,
1624 post_build: vec![],
1625 })
1626 }
1627}
1628
1629fn bridge_handle_path(api: &ApiSurface, bridge: &alef_core::config::TraitBridgeConfig, core_import: &str) -> String {
1630 let alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
1631 api.types
1632 .iter()
1633 .find(|t| t.name == alias && !t.rust_path.is_empty())
1634 .map(|t| t.rust_path.replace('-', "_"))
1635 .or_else(|| api.excluded_type_paths.get(alias).map(|path| path.replace('-', "_")))
1636 .unwrap_or_else(|| format!("{core_import}::visitor::{alias}"))
1637}
1638
1639fn php_phpdoc_type(ty: &TypeRef) -> String {
1642 match ty {
1643 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1644 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1645 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1646 _ => php_type(ty),
1647 }
1648}
1649
1650fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1652 match ty {
1653 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1654 TypeRef::Map(k, v) => format!(
1655 "array<{}, {}>",
1656 php_phpdoc_type_fq(k, namespace),
1657 php_phpdoc_type_fq(v, namespace)
1658 ),
1659 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1660 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1661 _ => php_type(ty),
1662 }
1663}
1664
1665fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1667 match ty {
1668 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1669 TypeRef::Optional(inner) => {
1670 let inner_type = php_type_fq(inner, namespace);
1671 if inner_type.starts_with('?') {
1672 inner_type
1673 } else {
1674 format!("?{inner_type}")
1675 }
1676 }
1677 _ => php_type(ty),
1678 }
1679}
1680
1681fn gen_php_opaque_class_file(
1688 typ: &alef_core::ir::TypeDef,
1689 namespace: &str,
1690 streaming_adapters: &[&alef_core::config::AdapterConfig],
1691 streaming_method_names: &AHashSet<String>,
1692) -> String {
1693 let mut content = String::new();
1694 content.push_str(&crate::template_env::render(
1695 "php_file_header.jinja",
1696 minijinja::Value::default(),
1697 ));
1698 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1699 content.push_str(&crate::template_env::render(
1700 "php_declare_strict_types.jinja",
1701 minijinja::Value::default(),
1702 ));
1703 content.push('\n');
1705 content.push_str(&crate::template_env::render(
1706 "php_namespace.jinja",
1707 context! { namespace => namespace },
1708 ));
1709 content.push('\n');
1711
1712 if !typ.doc.is_empty() {
1714 content.push_str("/**\n");
1715 let sanitized = sanitize_rust_idioms(&typ.doc, DocTarget::PhpDoc);
1716 content.push_str(&crate::template_env::render(
1717 "php_phpdoc_lines.jinja",
1718 context! {
1719 doc_lines => sanitized.lines().collect::<Vec<_>>(),
1720 indent => "",
1721 },
1722 ));
1723 content.push_str(" */\n");
1724 }
1725
1726 content.push_str(&format!("final class {}\n{{\n", typ.name));
1727
1728 let mut method_order: Vec<&alef_core::ir::MethodDef> = Vec::new();
1731 method_order.extend(
1732 typ.methods
1733 .iter()
1734 .filter(|m| m.receiver.is_some() && !streaming_method_names.contains(&m.name)),
1735 );
1736 method_order.extend(
1737 typ.methods
1738 .iter()
1739 .filter(|m| m.receiver.is_none() && !streaming_method_names.contains(&m.name)),
1740 );
1741
1742 for method in method_order {
1743 let method_name = method.name.to_lower_camel_case();
1744 let return_type = php_type(&method.return_type);
1745 let is_void = matches!(&method.return_type, TypeRef::Unit);
1746 let is_static = method.receiver.is_none();
1747
1748 let mut doc_lines: Vec<String> = vec![];
1750 let sanitized = sanitize_rust_idioms(&method.doc, DocTarget::PhpDoc);
1751 let doc_line = sanitized.lines().next().unwrap_or("").trim();
1752 if !doc_line.is_empty() {
1753 doc_lines.push(doc_line.to_string());
1754 }
1755
1756 let mut phpdoc_params: Vec<String> = vec![];
1758 for param in &method.params {
1759 if matches!(¶m.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)) {
1760 let phpdoc_type = php_phpdoc_type(¶m.ty);
1761 phpdoc_params.push(format!("@param {} ${}", phpdoc_type, param.name));
1762 }
1763 }
1764 doc_lines.extend(phpdoc_params);
1765
1766 let needs_return_phpdoc = matches!(&method.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _));
1768 if needs_return_phpdoc {
1769 let phpdoc_type = php_phpdoc_type(&method.return_type);
1770 doc_lines.push(format!("@return {phpdoc_type}"));
1771 }
1772
1773 if !doc_lines.is_empty() {
1775 content.push_str(" /**\n");
1776 for line in doc_lines {
1777 content.push_str(&format!(" * {}\n", line));
1778 }
1779 content.push_str(" */\n");
1780 }
1781
1782 let static_kw = if is_static { "static " } else { "" };
1784 let first_optional_idx = method.params.iter().position(|p| p.optional);
1785 let params: Vec<String> = method
1786 .params
1787 .iter()
1788 .enumerate()
1789 .map(|(idx, p)| {
1790 let ptype = php_type(&p.ty);
1791 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1792 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1793 format!("{nullable}{ptype} ${} = null", p.name)
1794 } else {
1795 format!("{} ${}", ptype, p.name)
1796 }
1797 })
1798 .collect();
1799 content.push_str(&format!(
1800 " public {static_kw}function {method_name}({}): {return_type}\n",
1801 params.join(", ")
1802 ));
1803 let body = if is_void {
1804 " {\n }\n"
1805 } else {
1806 " {\n throw new \\RuntimeException('Not implemented — provided by the native extension.');\n }\n"
1807 };
1808 content.push_str(body);
1809 }
1810
1811 for adapter in streaming_adapters {
1813 let item_type = adapter.item_type.as_deref().unwrap_or("array");
1814 content.push_str(&gen_php_streaming_method_wrapper(adapter, item_type));
1815 content.push('\n');
1816 }
1817
1818 content.push_str("}\n");
1819 content
1820}
1821
1822fn gen_php_streaming_method_wrapper(adapter: &alef_core::config::AdapterConfig, _item_type: &str) -> String {
1828 let method_name = adapter.name.to_lower_camel_case();
1829
1830 let mut params_vec: Vec<String> = Vec::new();
1832
1833 for p in &adapter.params {
1834 let ptype = php_type(&alef_core::ir::TypeRef::Named(p.ty.clone()));
1835 let nullable = if p.optional { "?" } else { "" };
1836 let default = if p.optional { " = null" } else { "" };
1837 params_vec.push(format!("{nullable}{ptype} ${}{default}", p.name));
1838 }
1839
1840 let params_sig = params_vec.join(", ");
1841
1842 format!(
1847 " public function {method_name}({params_sig}): \\Generator\n {{\n \
1848 throw new \\RuntimeException('Not implemented — provided by the native extension.');\n \
1849 }}\n",
1850 method_name = method_name,
1851 )
1852}
1853
1854fn php_type(ty: &TypeRef) -> String {
1856 match ty {
1857 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1858 TypeRef::Primitive(p) => match p {
1859 PrimitiveType::Bool => "bool".to_string(),
1860 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1861 PrimitiveType::U8
1862 | PrimitiveType::U16
1863 | PrimitiveType::U32
1864 | PrimitiveType::U64
1865 | PrimitiveType::I8
1866 | PrimitiveType::I16
1867 | PrimitiveType::I32
1868 | PrimitiveType::I64
1869 | PrimitiveType::Usize
1870 | PrimitiveType::Isize => "int".to_string(),
1871 },
1872 TypeRef::Optional(inner) => {
1873 let inner_type = php_type(inner);
1876 if inner_type.starts_with('?') {
1877 inner_type
1878 } else {
1879 format!("?{inner_type}")
1880 }
1881 }
1882 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1883 TypeRef::Named(name) => name.clone(),
1884 TypeRef::Unit => "void".to_string(),
1885 TypeRef::Duration => "float".to_string(),
1886 }
1887}
1888
1889fn php_property_phpdoc(var_type: &str, doc: &str, indent: &str) -> String {
1898 let doc = doc.trim();
1899 if doc.is_empty() {
1900 return format!("{indent}/** @var {var_type} */\n");
1901 }
1902 let lines: Vec<&str> = doc.lines().collect();
1903 if lines.len() == 1 {
1904 let line = lines[0].trim();
1905 return format!("{indent}/** @var {var_type} {line} */\n");
1906 }
1907 let mut out = format!("{indent}/**\n");
1909 for line in &lines {
1910 let trimmed = line.trim();
1911 if trimmed.is_empty() {
1912 out.push_str(&format!("{indent} *\n"));
1913 } else {
1914 out.push_str(&format!("{indent} * {trimmed}\n"));
1915 }
1916 }
1917 out.push_str(&format!("{indent} *\n"));
1918 out.push_str(&format!("{indent} * @var {var_type}\n"));
1919 out.push_str(&format!("{indent} */\n"));
1920 out
1921}