1mod functions;
2mod helpers;
3mod 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_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
12use alef_core::config::{Language, ResolvedCrateConfig, detect_serde_available, resolve_output_dir};
13use alef_core::hash::{self, CommentStyle};
14use alef_core::ir::ApiSurface;
15use alef_core::ir::{PrimitiveType, TypeRef};
16use heck::{ToLowerCamelCase, ToPascalCase};
17use minijinja::context;
18use std::path::PathBuf;
19
20use crate::naming::php_autoload_namespace;
21use functions::{gen_async_function_as_static_method, gen_function_as_static_method};
22
23fn sanitize_php_enum_case(name: &str) -> String {
26 if name.eq_ignore_ascii_case("class") {
27 format!("{name}_")
28 } else {
29 name.to_string()
30 }
31}
32use helpers::{gen_enum_tainted_from_binding_to_core, gen_tokio_runtime, has_enum_named_field, references_named_type};
33use types::{
34 gen_enum_constants, gen_flat_data_enum, gen_flat_data_enum_from_impls, gen_flat_data_enum_methods,
35 gen_opaque_struct_methods, gen_php_struct, is_tagged_data_enum, is_untagged_data_enum,
36};
37
38pub struct PhpBackend;
39
40impl PhpBackend {
41 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
42 RustBindingConfig {
43 struct_attrs: &["php_class"],
44 field_attrs: &[],
45 struct_derives: &["Clone"],
46 method_block_attr: Some("php_impl"),
47 constructor_attr: "",
48 static_attr: None,
49 function_attr: "#[php_function]",
50 enum_attrs: &[],
51 enum_derives: &[],
52 needs_signature: false,
53 signature_prefix: "",
54 signature_suffix: "",
55 core_import,
56 async_pattern: AsyncPattern::TokioBlockOn,
57 has_serde,
58 type_name_prefix: "",
59 option_duration_on_defaults: true,
60 opaque_type_names: &[],
61 skip_impl_constructor: false,
62 cast_uints_to_i32: false,
63 cast_large_ints_to_f64: false,
64 named_non_opaque_params_by_ref: false,
65 lossy_skip_types: &[],
66 serializable_opaque_type_names: &[],
67 never_skip_cfg_field_names: &[],
68 }
69 }
70}
71
72impl Backend for PhpBackend {
73 fn name(&self) -> &str {
74 "php"
75 }
76
77 fn language(&self) -> Language {
78 Language::Php
79 }
80
81 fn capabilities(&self) -> Capabilities {
82 Capabilities {
83 supports_async: false,
84 supports_classes: true,
85 supports_enums: true,
86 supports_option: true,
87 supports_result: true,
88 ..Capabilities::default()
89 }
90 }
91
92 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
93 let data_enum_names: AHashSet<String> = api
96 .enums
97 .iter()
98 .filter(|e| is_tagged_data_enum(e))
99 .map(|e| e.name.clone())
100 .collect();
101 let untagged_data_enum_names: AHashSet<String> = api
102 .enums
103 .iter()
104 .filter(|e| is_untagged_data_enum(e))
105 .map(|e| e.name.clone())
106 .collect();
107 let enum_names: AHashSet<String> = api
110 .enums
111 .iter()
112 .filter(|e| !is_tagged_data_enum(e) && !is_untagged_data_enum(e))
113 .map(|e| e.name.clone())
114 .collect();
115 let mapper = PhpMapper {
116 enum_names: enum_names.clone(),
117 data_enum_names: data_enum_names.clone(),
118 untagged_data_enum_names: untagged_data_enum_names.clone(),
119 };
120 let core_import = config.core_import_name();
121 let lang_rename_all = config.serde_rename_all_for_language(Language::Php);
122
123 let php_config = config.php.as_ref();
125 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
126 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
127
128 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
129 let has_serde = detect_serde_available(&output_dir);
130
131 let bridge_type_aliases_php: Vec<String> = config
137 .trait_bridges
138 .iter()
139 .filter_map(|b| b.type_alias.clone())
140 .collect();
141 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
142 let mut opaque_names_vec_php: Vec<String> = api
143 .types
144 .iter()
145 .filter(|t| t.is_opaque)
146 .map(|t| t.name.clone())
147 .collect();
148 opaque_names_vec_php.extend(bridge_type_aliases_php);
149
150 let mut cfg = Self::binding_config(&core_import, has_serde);
151 cfg.opaque_type_names = &opaque_names_vec_php;
152 let never_skip_cfg_field_names: Vec<String> = config
153 .trait_bridges
154 .iter()
155 .filter_map(|b| {
156 if b.bind_via == alef_core::config::BridgeBinding::OptionsField {
157 b.resolved_options_field().map(|s| s.to_string())
158 } else {
159 None
160 }
161 })
162 .collect();
163 cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;
164
165 let mut builder = RustFileBuilder::new().with_generated_header();
167 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
168 builder.add_inner_attribute("allow(unsafe_code)");
169 builder.add_inner_attribute("allow(non_snake_case)");
171 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)");
172 builder.add_import("ext_php_rs::prelude::*");
173
174 if has_serde {
176 builder.add_import("serde_json");
177 }
178
179 for trait_path in generators::collect_trait_imports(api) {
181 builder.add_import(&trait_path);
182 }
183
184 let has_maps = api.types.iter().any(|t| {
186 t.fields
187 .iter()
188 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
189 }) || api
190 .functions
191 .iter()
192 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
193 if has_maps {
194 builder.add_import("std::collections::HashMap");
195 }
196
197 builder.add_item(
202 "#[derive(Debug, Clone, Default)]\n\
203 pub struct PhpBytes(pub Vec<u8>);\n\
204 \n\
205 impl<'a> ext_php_rs::convert::FromZval<'a> for PhpBytes {\n \
206 const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::String;\n \
207 fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {\n \
208 zval.zend_str().map(|zs| PhpBytes(zs.as_bytes().to_vec()))\n \
209 }\n\
210 }\n\
211 \n\
212 impl From<PhpBytes> for Vec<u8> {\n \
213 fn from(b: PhpBytes) -> Self { b.0 }\n\
214 }\n\
215 \n\
216 impl From<Vec<u8>> for PhpBytes {\n \
217 fn from(v: Vec<u8>) -> Self { PhpBytes(v) }\n\
218 }\n",
219 );
220
221 let custom_mods = config.custom_modules.for_language(Language::Php);
223 for module in custom_mods {
224 builder.add_item(&format!("pub mod {module};"));
225 }
226
227 let has_async =
229 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
230
231 if has_async {
232 builder.add_item(&gen_tokio_runtime());
233 }
234
235 let opaque_types: AHashSet<String> = api
237 .types
238 .iter()
239 .filter(|t| t.is_opaque)
240 .map(|t| t.name.clone())
241 .collect();
242 if !opaque_types.is_empty() {
243 builder.add_import("std::sync::Arc");
244 }
245
246 let mutex_types: AHashSet<String> = api
248 .types
249 .iter()
250 .filter(|t| t.is_opaque && alef_codegen::generators::type_needs_mutex(t))
251 .map(|t| t.name.clone())
252 .collect();
253 if !mutex_types.is_empty() {
254 builder.add_import("std::sync::Mutex");
255 }
256
257 let extension_name = config.php_extension_name();
260 let php_namespace = php_autoload_namespace(config);
261
262 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
264
265 for adapter in &config.adapters {
267 match adapter.pattern {
268 alef_core::config::AdapterPattern::Streaming => {
269 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
270 if let Some(struct_code) = adapter_bodies.get(&key) {
271 builder.add_item(struct_code);
272 }
273 }
274 alef_core::config::AdapterPattern::CallbackBridge => {
275 let struct_key = format!("{}.__bridge_struct__", adapter.name);
276 let impl_key = format!("{}.__bridge_impl__", adapter.name);
277 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
278 builder.add_item(struct_code);
279 }
280 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
281 builder.add_item(impl_code);
282 }
283 }
284 _ => {}
285 }
286 }
287
288 for typ in api
289 .types
290 .iter()
291 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
292 {
293 if typ.is_opaque {
294 let ns_escaped = php_namespace.replace('\\', "\\\\");
298 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
299 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
300 let opaque_cfg = RustBindingConfig {
301 struct_attrs: &opaque_attr_arr,
302 ..cfg
303 };
304 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
305 builder.add_item(&gen_opaque_struct_methods(
306 typ,
307 &mapper,
308 &opaque_types,
309 &core_import,
310 &adapter_bodies,
311 &mutex_types,
312 ));
313 } else {
314 builder.add_item(&gen_php_struct(
317 typ,
318 &mapper,
319 &cfg,
320 Some(&php_namespace),
321 &enum_names,
322 &lang_rename_all,
323 ));
324 builder.add_item(&types::gen_struct_methods_with_exclude(
325 typ,
326 &mapper,
327 has_serde,
328 &core_import,
329 &opaque_types,
330 &enum_names,
331 &api.enums,
332 &exclude_functions,
333 &bridge_type_aliases_set,
334 &never_skip_cfg_field_names,
335 &mutex_types,
336 ));
337 }
338 }
339
340 for enum_def in &api.enums {
341 if is_tagged_data_enum(enum_def) {
342 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
344 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
345 } else {
346 builder.add_item(&gen_enum_constants(enum_def));
347 }
348 }
349
350 let included_functions: Vec<_> = api
355 .functions
356 .iter()
357 .filter(|f| !exclude_functions.contains(&f.name))
358 .collect();
359 if !included_functions.is_empty() {
360 let facade_class_name = extension_name.to_pascal_case();
361 let mut method_items: Vec<String> = Vec::new();
364 for func in included_functions {
365 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
366 if let Some((param_idx, bridge_cfg)) = bridge_param {
367 let bridge_handle_path = bridge_handle_path(api, bridge_cfg, &core_import);
368 method_items.push(crate::trait_bridge::gen_bridge_function(
369 func,
370 param_idx,
371 bridge_cfg,
372 &mapper,
373 &opaque_types,
374 &core_import,
375 &bridge_handle_path,
376 ));
377 } else if func.is_async {
378 method_items.push(gen_async_function_as_static_method(
379 func,
380 &mapper,
381 &opaque_types,
382 &core_import,
383 &config.trait_bridges,
384 &mutex_types,
385 ));
386 } else {
387 method_items.push(gen_function_as_static_method(
388 func,
389 &mapper,
390 &opaque_types,
391 &core_import,
392 &config.trait_bridges,
393 has_serde,
394 &mutex_types,
395 ));
396 }
397 }
398
399 let methods_joined = method_items
400 .iter()
401 .map(|m| {
402 m.lines()
404 .map(|l| {
405 if l.is_empty() {
406 String::new()
407 } else {
408 format!(" {l}")
409 }
410 })
411 .collect::<Vec<_>>()
412 .join("\n")
413 })
414 .collect::<Vec<_>>()
415 .join("\n\n");
416 let php_api_class_name = format!("{facade_class_name}Api");
419 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
421 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
422 let facade_struct = format!(
423 "#[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}}"
424 );
425 builder.add_item(&facade_struct);
426
427 for bridge_cfg in &config.trait_bridges {
429 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
430 let bridge = crate::trait_bridge::gen_trait_bridge(
431 trait_type,
432 bridge_cfg,
433 &core_import,
434 &config.error_type_name(),
435 &config.error_constructor_expr(),
436 api,
437 );
438 for imp in &bridge.imports {
439 builder.add_import(imp);
440 }
441 builder.add_item(&bridge.code);
442 }
443 }
444 }
445
446 let convertible = alef_codegen::conversions::convertible_types(api);
447 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
448 let input_types = alef_codegen::conversions::input_type_names(api);
449 let enum_names_ref = &mapper.enum_names;
454 let bridge_skip_types: Vec<String> = config
455 .trait_bridges
456 .iter()
457 .filter(|b| !matches!(b.bind_via, alef_core::config::BridgeBinding::OptionsField))
458 .filter_map(|b| b.type_alias.clone())
459 .collect();
460 let trait_bridge_arc_wrapper_field_names: Vec<String> = config
465 .trait_bridges
466 .iter()
467 .filter(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField)
468 .filter_map(|b| b.resolved_options_field().map(String::from))
469 .collect();
470 let mut conv_opaque_types: AHashSet<String> = opaque_types.clone();
475 for bridge in &config.trait_bridges {
476 if let Some(alias) = &bridge.type_alias {
477 conv_opaque_types.insert(alias.clone());
478 }
479 }
480 let php_conv_config = ConversionConfig {
481 cast_large_ints_to_i64: true,
482 enum_string_names: Some(enum_names_ref),
483 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
484 json_as_value: true,
488 include_cfg_metadata: false,
489 option_duration_on_defaults: true,
490 from_binding_skip_types: &bridge_skip_types,
491 never_skip_cfg_field_names: &never_skip_cfg_field_names,
492 opaque_types: Some(&conv_opaque_types),
493 trait_bridge_arc_wrapper_field_names: &trait_bridge_arc_wrapper_field_names,
494 ..Default::default()
495 };
496 let mut enum_tainted: AHashSet<String> = AHashSet::new();
498 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
499 if has_enum_named_field(typ, enum_names_ref) {
500 enum_tainted.insert(typ.name.clone());
501 }
502 }
503 let mut changed = true;
505 while changed {
506 changed = false;
507 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
508 if !enum_tainted.contains(&typ.name)
509 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
510 {
511 enum_tainted.insert(typ.name.clone());
512 changed = true;
513 }
514 }
515 }
516 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
517 if input_types.contains(&typ.name)
519 && !enum_tainted.contains(&typ.name)
520 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
521 {
522 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
523 typ,
524 &core_import,
525 &php_conv_config,
526 ));
527 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
528 builder.add_item(&gen_enum_tainted_from_binding_to_core(
535 typ,
536 &core_import,
537 enum_names_ref,
538 &enum_tainted,
539 &php_conv_config,
540 &api.enums,
541 &bridge_type_aliases_set,
542 ));
543 }
544 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
546 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
547 typ,
548 &core_import,
549 &opaque_types,
550 &php_conv_config,
551 ));
552 }
553 }
554
555 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
557 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
558 }
559
560 for error in &api.errors {
562 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
563 }
564
565 if has_serde {
569 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
570 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
571 pub fn max_compression_ratio() -> i64 { 100 }\n\
572 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
573 pub fn max_nesting_depth() -> i64 { 1024 }\n\
574 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
575 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
576 pub fn max_iterations() -> i64 { 10_000_000 }\n\
577 pub fn max_xml_depth() -> i64 { 1024 }\n\
578 pub fn max_table_cells() -> i64 { 100_000 }\n\
579 }";
580 builder.add_item(serde_module);
581 }
582
583 let php_config = config.php.as_ref();
589 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
590
591 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
595 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
596 }
597
598 let mut class_registrations = String::new();
601 for typ in api
602 .types
603 .iter()
604 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
605 {
606 class_registrations.push_str(&crate::template_env::render(
607 "php_class_registration.jinja",
608 context! { class_name => &typ.name },
609 ));
610 }
611 if !api.functions.is_empty() {
613 let facade_class_name = extension_name.to_pascal_case();
614 class_registrations.push_str(&crate::template_env::render(
615 "php_class_registration.jinja",
616 context! { class_name => &format!("{facade_class_name}Api") },
617 ));
618 }
619 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
622 class_registrations.push_str(&crate::template_env::render(
623 "php_class_registration.jinja",
624 context! { class_name => &enum_def.name },
625 ));
626 }
627 builder.add_item(&format!(
628 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
629 ));
630
631 let mut content = builder.build();
632
633 for bridge in &config.trait_bridges {
638 if let Some(field_name) = bridge.resolved_options_field() {
639 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
640 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
641 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
642 let builder_type = format!("{}Builder", options_type);
643 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
644 let bridge_handle_path = bridge_handle_path(api, bridge, &core_import);
645
646 let old_method = format!(
652 " 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 }}"
653 );
654 let new_method = format!(
655 " 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 }}"
656 );
657
658 content = content.replace(&old_method, &new_method);
659 }
660 }
661
662 Ok(vec![GeneratedFile {
663 path: PathBuf::from(&output_dir).join("lib.rs"),
664 content,
665 generated_header: false,
666 }])
667 }
668
669 fn generate_public_api(
670 &self,
671 api: &ApiSurface,
672 config: &ResolvedCrateConfig,
673 ) -> anyhow::Result<Vec<GeneratedFile>> {
674 let extension_name = config.php_extension_name();
675 let class_name = extension_name.to_pascal_case();
676
677 let mut content = String::new();
679 content.push_str(&crate::template_env::render(
680 "php_file_header.jinja",
681 minijinja::Value::default(),
682 ));
683 content.push_str(&hash::header(CommentStyle::DoubleSlash));
684 content.push_str(&crate::template_env::render(
685 "php_declare_strict_types.jinja",
686 minijinja::Value::default(),
687 ));
688
689 let namespace = php_autoload_namespace(config);
691
692 content.push_str(&crate::template_env::render(
693 "php_namespace.jinja",
694 context! { namespace => &namespace },
695 ));
696 content.push_str(&crate::template_env::render(
697 "php_facade_class_declaration.jinja",
698 context! { class_name => &class_name },
699 ));
700
701 let bridge_param_names_pub: ahash::AHashSet<&str> = config
703 .trait_bridges
704 .iter()
705 .filter_map(|b| b.param_name.as_deref())
706 .collect();
707
708 let no_arg_constructor_types: AHashSet<String> = api
713 .types
714 .iter()
715 .filter(|t| t.fields.iter().all(|f| f.optional))
716 .map(|t| t.name.clone())
717 .collect();
718
719 for func in &api.functions {
721 let method_name = func.name.to_lower_camel_case();
726 let return_php_type = php_type(&func.return_type);
727
728 let visible_params: Vec<_> = func
730 .params
731 .iter()
732 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
733 .collect();
734
735 content.push_str(&crate::template_env::render(
737 "php_phpdoc_block_start.jinja",
738 minijinja::Value::default(),
739 ));
740 if func.doc.is_empty() {
741 content.push_str(&crate::template_env::render(
742 "php_phpdoc_text_line.jinja",
743 context! { text => &format!("{}.", method_name) },
744 ));
745 } else {
746 content.push_str(&crate::template_env::render(
747 "php_phpdoc_lines.jinja",
748 context! {
749 doc_lines => func.doc.lines().collect::<Vec<_>>(),
750 indent => " ",
751 },
752 ));
753 }
754 content.push_str(&crate::template_env::render(
755 "php_phpdoc_empty_line.jinja",
756 minijinja::Value::default(),
757 ));
758 for p in &visible_params {
759 let ptype = php_phpdoc_type(&p.ty);
760 let nullable_prefix = if p.optional { "?" } else { "" };
761 content.push_str(&crate::template_env::render(
762 "php_phpdoc_param_line.jinja",
763 context! {
764 nullable_prefix => nullable_prefix,
765 param_type => &ptype,
766 param_name => &p.name,
767 },
768 ));
769 }
770 let return_phpdoc = php_phpdoc_type(&func.return_type);
771 content.push_str(&crate::template_env::render(
772 "php_phpdoc_return_line.jinja",
773 context! { return_type => &return_phpdoc },
774 ));
775 if func.error_type.is_some() {
776 content.push_str(&crate::template_env::render(
777 "php_phpdoc_throws_line.jinja",
778 context! {
779 namespace => namespace.as_str(),
780 class_name => &class_name,
781 },
782 ));
783 }
784 content.push_str(&crate::template_env::render(
785 "php_phpdoc_block_end.jinja",
786 minijinja::Value::default(),
787 ));
788
789 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
800 if let TypeRef::Named(name) = &p.ty {
801 (name.ends_with("Config") || name.as_str() == "config")
802 && no_arg_constructor_types.contains(name.as_str())
803 } else {
804 false
805 }
806 };
807
808 let mut first_optional_idx = None;
809 for (idx, p) in visible_params.iter().enumerate() {
810 if p.optional || is_optional_config_param(p) {
811 first_optional_idx = Some(idx);
812 break;
813 }
814 }
815
816 content.push_str(&crate::template_env::render(
817 "php_method_signature_start.jinja",
818 context! { method_name => &method_name },
819 ));
820
821 let params: Vec<String> = visible_params
822 .iter()
823 .enumerate()
824 .map(|(idx, p)| {
825 let ptype = php_type(&p.ty);
826 let should_be_optional = p.optional
831 || is_optional_config_param(p)
832 || first_optional_idx.is_some_and(|first| idx >= first);
833 if should_be_optional {
834 format!("?{} ${} = null", ptype, p.name)
835 } else {
836 format!("{} ${}", ptype, p.name)
837 }
838 })
839 .collect();
840 content.push_str(¶ms.join(", "));
841 content.push_str(&crate::template_env::render(
842 "php_method_signature_end.jinja",
843 context! { return_type => &return_php_type },
844 ));
845 let ext_method_name = func.name.to_lower_camel_case();
850 let is_void = matches!(&func.return_type, TypeRef::Unit);
851 let call_params = visible_params
859 .iter()
860 .enumerate()
861 .map(|(idx, p)| {
862 let should_be_optional = p.optional
863 || is_optional_config_param(p)
864 || first_optional_idx.is_some_and(|first| idx >= first);
865 if should_be_optional && is_optional_config_param(p) {
866 if let TypeRef::Named(type_name) = &p.ty {
867 return format!("${} ?? new {}()", p.name, type_name);
868 }
869 }
870 format!("${}", p.name)
871 })
872 .collect::<Vec<_>>()
873 .join(", ");
874 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
875 if is_void {
876 content.push_str(&crate::template_env::render(
877 "php_method_call_statement.jinja",
878 context! { call_expr => &call_expr },
879 ));
880 } else {
881 content.push_str(&crate::template_env::render(
882 "php_method_call_return.jinja",
883 context! { call_expr => &call_expr },
884 ));
885 }
886 content.push_str(&crate::template_env::render(
887 "php_method_end.jinja",
888 minijinja::Value::default(),
889 ));
890 }
891
892 content.push_str(&crate::template_env::render(
893 "php_class_end.jinja",
894 minijinja::Value::default(),
895 ));
896
897 let output_dir = config
901 .php
902 .as_ref()
903 .and_then(|p| p.stubs.as_ref())
904 .map(|s| s.output.to_string_lossy().to_string())
905 .unwrap_or_else(|| "packages/php/src/".to_string());
906
907 let mut files: Vec<GeneratedFile> = Vec::new();
908 files.push(GeneratedFile {
909 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
910 content,
911 generated_header: false,
912 });
913
914 for typ in api.types.iter().filter(|t| t.is_opaque && !t.is_trait) {
920 let opaque_file = gen_php_opaque_class_file(typ, &namespace);
921 files.push(GeneratedFile {
922 path: PathBuf::from(&output_dir).join(format!("{}.php", typ.name)),
923 content: opaque_file,
924 generated_header: false,
925 });
926 }
927
928 Ok(files)
929 }
930
931 fn generate_type_stubs(
932 &self,
933 api: &ApiSurface,
934 config: &ResolvedCrateConfig,
935 ) -> anyhow::Result<Vec<GeneratedFile>> {
936 let extension_name = config.php_extension_name();
937 let class_name = extension_name.to_pascal_case();
938
939 let namespace = php_autoload_namespace(config);
941
942 let mut content = String::new();
947 content.push_str(&crate::template_env::render(
948 "php_file_header.jinja",
949 minijinja::Value::default(),
950 ));
951 content.push_str(&hash::header(CommentStyle::DoubleSlash));
952 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
953 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
954 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
955 content.push_str(&crate::template_env::render(
956 "php_declare_strict_types.jinja",
957 minijinja::Value::default(),
958 ));
959 content.push_str(&crate::template_env::render(
961 "php_namespace_block_begin.jinja",
962 context! { namespace => &namespace },
963 ));
964
965 content.push_str(&crate::template_env::render(
967 "php_exception_class_declaration.jinja",
968 context! { class_name => &class_name },
969 ));
970 content.push_str(
971 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
972 );
973 content.push_str("}\n\n");
974
975 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
982 if typ.is_opaque || typ.fields.is_empty() {
983 continue;
984 }
985 if !typ.doc.is_empty() {
986 content.push_str("/**\n");
987 content.push_str(&crate::template_env::render(
988 "php_phpdoc_lines.jinja",
989 context! {
990 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
991 indent => "",
992 },
993 ));
994 content.push_str(" */\n");
995 }
996 content.push_str(&crate::template_env::render(
997 "php_record_class_stub_declaration.jinja",
998 context! { class_name => &typ.name },
999 ));
1000
1001 for field in &typ.fields {
1003 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
1004 let prop_type = if field.optional {
1005 let inner = php_type(&field.ty);
1006 if inner.starts_with('?') {
1007 inner
1008 } else {
1009 format!("?{inner}")
1010 }
1011 } else {
1012 php_type(&field.ty)
1013 };
1014 if is_array {
1015 let phpdoc = php_phpdoc_type(&field.ty);
1016 let nullable_prefix = if field.optional { "?" } else { "" };
1017 content.push_str(&crate::template_env::render(
1018 "php_property_type_annotation.jinja",
1019 context! {
1020 nullable_prefix => nullable_prefix,
1021 phpdoc => &phpdoc,
1022 },
1023 ));
1024 }
1025 content.push_str(&crate::template_env::render(
1026 "php_property_stub.jinja",
1027 context! {
1028 prop_type => &prop_type,
1029 field_name => &field.name,
1030 },
1031 ));
1032 }
1033 content.push('\n');
1034
1035 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
1039 sorted_fields.sort_by_key(|f| f.optional);
1040
1041 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
1044 .iter()
1045 .copied()
1046 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
1047 .collect();
1048 if !array_fields.is_empty() {
1049 content.push_str(" /**\n");
1050 for f in &array_fields {
1051 let phpdoc = php_phpdoc_type(&f.ty);
1052 let nullable_prefix = if f.optional { "?" } else { "" };
1053 content.push_str(&crate::template_env::render(
1054 "php_phpdoc_array_param.jinja",
1055 context! {
1056 nullable_prefix => nullable_prefix,
1057 phpdoc => &phpdoc,
1058 param_name => &f.name,
1059 },
1060 ));
1061 }
1062 content.push_str(" */\n");
1063 }
1064
1065 let params: Vec<String> = sorted_fields
1066 .iter()
1067 .map(|f| {
1068 let ptype = php_type(&f.ty);
1069 let nullable = if f.optional && !ptype.starts_with('?') {
1070 format!("?{ptype}")
1071 } else {
1072 ptype
1073 };
1074 let default = if f.optional { " = null" } else { "" };
1075 format!(" {} ${}{}", nullable, f.name, default)
1076 })
1077 .collect();
1078 content.push_str(&crate::template_env::render(
1079 "php_constructor_method.jinja",
1080 context! { params => ¶ms.join(",\n") },
1081 ));
1082
1083 for field in &typ.fields {
1085 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
1086 let return_type = if field.optional {
1087 let inner = php_type(&field.ty);
1088 if inner.starts_with('?') {
1089 inner
1090 } else {
1091 format!("?{inner}")
1092 }
1093 } else {
1094 php_type(&field.ty)
1095 };
1096 let getter_name = field.name.to_lower_camel_case();
1097 if is_array {
1099 let phpdoc = php_phpdoc_type(&field.ty);
1100 let nullable_prefix = if field.optional { "?" } else { "" };
1101 content.push_str(&crate::template_env::render(
1102 "php_constructor_doc_return.jinja",
1103 context! { return_type => &format!("{nullable_prefix}{phpdoc}") },
1104 ));
1105 }
1106 let is_void_getter = return_type == "void";
1107 let getter_body = if is_void_getter {
1108 "{ }".to_string()
1109 } else {
1110 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1111 };
1112 content.push_str(&crate::template_env::render(
1113 "php_getter_stub.jinja",
1114 context! {
1115 getter_name => &format!("get{}", getter_name.to_pascal_case()),
1116 return_type => &return_type,
1117 getter_body => &getter_body,
1118 },
1119 ));
1120 }
1121
1122 content.push_str("}\n\n");
1123 }
1124
1125 for enum_def in &api.enums {
1128 if is_tagged_data_enum(enum_def) {
1129 if !enum_def.doc.is_empty() {
1131 content.push_str("/**\n");
1132 content.push_str(&crate::template_env::render(
1133 "php_phpdoc_lines.jinja",
1134 context! {
1135 doc_lines => enum_def.doc.lines().collect::<Vec<_>>(),
1136 indent => "",
1137 },
1138 ));
1139 content.push_str(" */\n");
1140 }
1141 content.push_str(&crate::template_env::render(
1142 "php_record_class_stub_declaration.jinja",
1143 context! { class_name => &enum_def.name },
1144 ));
1145 content.push_str("}\n\n");
1146 } else {
1147 content.push_str(&crate::template_env::render(
1149 "php_tagged_enum_declaration.jinja",
1150 context! { enum_name => &enum_def.name },
1151 ));
1152 for variant in &enum_def.variants {
1153 let case_name = sanitize_php_enum_case(&variant.name);
1154 content.push_str(&crate::template_env::render(
1155 "php_enum_variant_stub.jinja",
1156 context! {
1157 variant_name => case_name,
1158 value => &variant.name,
1159 },
1160 ));
1161 }
1162 content.push_str("}\n\n");
1163 }
1164 }
1165
1166 if !api.functions.is_empty() {
1171 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1173 .trait_bridges
1174 .iter()
1175 .filter_map(|b| b.param_name.as_deref())
1176 .collect();
1177
1178 content.push_str(&crate::template_env::render(
1179 "php_api_class_declaration.jinja",
1180 context! { class_name => &class_name },
1181 ));
1182 for func in &api.functions {
1183 let return_type = php_type_fq(&func.return_type, &namespace);
1184 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1185 let visible_params: Vec<_> = func
1187 .params
1188 .iter()
1189 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1190 .collect();
1191 let has_array_params = visible_params
1198 .iter()
1199 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1200 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1201 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1202 if has_array_params || has_array_return {
1203 content.push_str(" /**\n");
1204 for p in &visible_params {
1205 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1206 let nullable_prefix = if p.optional { "?" } else { "" };
1207 content.push_str(&crate::template_env::render(
1208 "php_phpdoc_static_param.jinja",
1209 context! {
1210 nullable_prefix => nullable_prefix,
1211 ptype => &ptype,
1212 param_name => &p.name,
1213 },
1214 ));
1215 }
1216 content.push_str(&crate::template_env::render(
1217 "php_phpdoc_static_return.jinja",
1218 context! { return_phpdoc => &return_phpdoc },
1219 ));
1220 content.push_str(" */\n");
1221 }
1222 let params: Vec<String> = visible_params
1223 .iter()
1224 .map(|p| {
1225 let ptype = php_type_fq(&p.ty, &namespace);
1226 if p.optional {
1227 format!("?{} ${} = null", ptype, p.name)
1228 } else {
1229 format!("{} ${}", ptype, p.name)
1230 }
1231 })
1232 .collect();
1233 let stub_method_name = func.name.to_lower_camel_case();
1237 let is_void_stub = return_type == "void";
1238 let stub_body = if is_void_stub {
1239 "{ }".to_string()
1240 } else {
1241 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1242 };
1243 content.push_str(&crate::template_env::render(
1244 "php_static_method_stub.jinja",
1245 context! {
1246 method_name => &stub_method_name,
1247 params => ¶ms.join(", "),
1248 return_type => &return_type,
1249 stub_body => &stub_body,
1250 },
1251 ));
1252 }
1253 content.push_str("}\n\n");
1254 }
1255
1256 content.push_str(&crate::template_env::render(
1258 "php_namespace_block_end.jinja",
1259 minijinja::Value::default(),
1260 ));
1261
1262 let output_dir = config
1264 .php
1265 .as_ref()
1266 .and_then(|p| p.stubs.as_ref())
1267 .map(|s| s.output.to_string_lossy().to_string())
1268 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1269
1270 Ok(vec![GeneratedFile {
1271 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1272 content,
1273 generated_header: false,
1274 }])
1275 }
1276
1277 fn build_config(&self) -> Option<BuildConfig> {
1278 Some(BuildConfig {
1279 tool: "cargo",
1280 crate_suffix: "-php",
1281 build_dep: BuildDependency::None,
1282 post_build: vec![],
1283 })
1284 }
1285}
1286
1287fn bridge_handle_path(api: &ApiSurface, bridge: &alef_core::config::TraitBridgeConfig, core_import: &str) -> String {
1288 let alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
1289 api.types
1290 .iter()
1291 .find(|t| t.name == alias && !t.rust_path.is_empty())
1292 .map(|t| t.rust_path.replace('-', "_"))
1293 .or_else(|| api.excluded_type_paths.get(alias).map(|path| path.replace('-', "_")))
1294 .unwrap_or_else(|| format!("{core_import}::visitor::{alias}"))
1295}
1296
1297fn php_phpdoc_type(ty: &TypeRef) -> String {
1300 match ty {
1301 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1302 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1303 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1304 _ => php_type(ty),
1305 }
1306}
1307
1308fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1310 match ty {
1311 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1312 TypeRef::Map(k, v) => format!(
1313 "array<{}, {}>",
1314 php_phpdoc_type_fq(k, namespace),
1315 php_phpdoc_type_fq(v, namespace)
1316 ),
1317 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1318 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1319 _ => php_type(ty),
1320 }
1321}
1322
1323fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1325 match ty {
1326 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1327 TypeRef::Optional(inner) => {
1328 let inner_type = php_type_fq(inner, namespace);
1329 if inner_type.starts_with('?') {
1330 inner_type
1331 } else {
1332 format!("?{inner_type}")
1333 }
1334 }
1335 _ => php_type(ty),
1336 }
1337}
1338
1339fn gen_php_opaque_class_file(typ: &alef_core::ir::TypeDef, namespace: &str) -> String {
1346 let mut content = String::new();
1347 content.push_str(&crate::template_env::render(
1348 "php_file_header.jinja",
1349 minijinja::Value::default(),
1350 ));
1351 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1352 content.push_str(&crate::template_env::render(
1353 "php_declare_strict_types.jinja",
1354 minijinja::Value::default(),
1355 ));
1356 content.push_str(&crate::template_env::render(
1357 "php_namespace.jinja",
1358 context! { namespace => namespace },
1359 ));
1360
1361 if !typ.doc.is_empty() {
1363 content.push_str("/**\n");
1364 content.push_str(&crate::template_env::render(
1365 "php_phpdoc_lines.jinja",
1366 context! {
1367 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
1368 indent => "",
1369 },
1370 ));
1371 content.push_str(" */\n");
1372 }
1373
1374 content.push_str(&format!("final class {}\n{{\n", typ.name));
1375
1376 let mut method_order: Vec<&alef_core::ir::MethodDef> = Vec::new();
1378 method_order.extend(typ.methods.iter().filter(|m| m.receiver.is_some()));
1379 method_order.extend(typ.methods.iter().filter(|m| m.receiver.is_none()));
1380
1381 for method in method_order {
1382 let method_name = method.name.to_lower_camel_case();
1383 let return_type = php_type(&method.return_type);
1384 let is_void = matches!(&method.return_type, TypeRef::Unit);
1385 let is_static = method.receiver.is_none();
1386
1387 let mut doc_lines: Vec<String> = vec![];
1389 let doc_line = method.doc.lines().next().unwrap_or("").trim();
1390 if !doc_line.is_empty() {
1391 doc_lines.push(doc_line.to_string());
1392 }
1393
1394 let mut phpdoc_params: Vec<String> = vec![];
1396 for param in &method.params {
1397 if matches!(¶m.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)) {
1398 let phpdoc_type = php_phpdoc_type(¶m.ty);
1399 phpdoc_params.push(format!("@param {} ${}", phpdoc_type, param.name));
1400 }
1401 }
1402 doc_lines.extend(phpdoc_params);
1403
1404 let needs_return_phpdoc = matches!(&method.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _));
1406 if needs_return_phpdoc {
1407 let phpdoc_type = php_phpdoc_type(&method.return_type);
1408 doc_lines.push(format!("@return {phpdoc_type}"));
1409 }
1410
1411 if !doc_lines.is_empty() {
1413 content.push_str(" /**\n");
1414 for line in doc_lines {
1415 content.push_str(&format!(" * {}\n", line));
1416 }
1417 content.push_str(" */\n");
1418 }
1419
1420 let static_kw = if is_static { "static " } else { "" };
1422 let params: Vec<String> = method
1423 .params
1424 .iter()
1425 .map(|p| {
1426 let ptype = php_type(&p.ty);
1427 if p.optional {
1428 format!("?{} ${} = null", ptype, p.name)
1429 } else {
1430 format!("{} ${}", ptype, p.name)
1431 }
1432 })
1433 .collect();
1434 content.push_str(&format!(
1435 " public {static_kw}function {method_name}({}): {return_type}\n",
1436 params.join(", ")
1437 ));
1438 let body = if is_void {
1439 " {\n }\n"
1440 } else {
1441 " {\n throw new \\RuntimeException('Not implemented — provided by the native extension.');\n }\n"
1442 };
1443 content.push_str(body);
1444 }
1445
1446 content.push_str("}\n");
1447 content
1448}
1449
1450fn php_type(ty: &TypeRef) -> String {
1452 match ty {
1453 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1454 TypeRef::Primitive(p) => match p {
1455 PrimitiveType::Bool => "bool".to_string(),
1456 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1457 PrimitiveType::U8
1458 | PrimitiveType::U16
1459 | PrimitiveType::U32
1460 | PrimitiveType::U64
1461 | PrimitiveType::I8
1462 | PrimitiveType::I16
1463 | PrimitiveType::I32
1464 | PrimitiveType::I64
1465 | PrimitiveType::Usize
1466 | PrimitiveType::Isize => "int".to_string(),
1467 },
1468 TypeRef::Optional(inner) => {
1469 let inner_type = php_type(inner);
1472 if inner_type.starts_with('?') {
1473 inner_type
1474 } else {
1475 format!("?{inner_type}")
1476 }
1477 }
1478 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1479 TypeRef::Named(name) => name.clone(),
1480 TypeRef::Unit => "void".to_string(),
1481 TypeRef::Duration => "float".to_string(),
1482 }
1483}