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