1mod functions;
2mod helpers;
3pub mod types;
4
5use crate::type_map::PhpMapper;
6use ahash::AHashSet;
7use alef_codegen::builder::RustFileBuilder;
8use alef_codegen::conversions::ConversionConfig;
9use alef_codegen::generators::RustBindingConfig;
10use alef_codegen::generators::{self, AsyncPattern};
11use alef_codegen::naming::to_php_name;
12use alef_codegen::shared::binding_fields;
13use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
14use alef_core::config::{Language, ResolvedCrateConfig, detect_serde_available, resolve_output_dir};
15use alef_core::hash::{self, CommentStyle};
16use alef_core::ir::ApiSurface;
17use alef_core::ir::{PrimitiveType, TypeRef};
18use heck::{ToLowerCamelCase, ToPascalCase};
19use minijinja::context;
20use std::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 = alef_adapters::stream_struct_key(adapter);
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 if let Some(ctor) = config.client_constructors.get(&typ.name) {
317 let ctor_body = generators::gen_opaque_constructor(ctor, &typ.name, &core_import, "#[php_method]");
318 let ctor_impl = format!("#[php_impl]\nimpl {} {{\n{}}}", typ.name, ctor_body);
319 builder.add_item(&ctor_impl);
320 }
321 } else {
322 builder.add_item(&gen_php_struct(
325 typ,
326 &mapper,
327 &cfg,
328 Some(&php_namespace),
329 &enum_names,
330 &lang_rename_all,
331 ));
332 builder.add_item(&types::gen_struct_methods_with_exclude(
333 typ,
334 &mapper,
335 has_serde,
336 &core_import,
337 &opaque_types,
338 &enum_names,
339 &api.enums,
340 &exclude_functions,
341 &bridge_type_aliases_set,
342 &never_skip_cfg_field_names,
343 &mutex_types,
344 ));
345 }
346 }
347
348 for enum_def in &api.enums {
349 if is_tagged_data_enum(enum_def) {
350 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
352 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
353 } else {
354 builder.add_item(&gen_enum_constants(enum_def));
355 }
356 }
357
358 let included_functions: Vec<_> = api
363 .functions
364 .iter()
365 .filter(|f| !exclude_functions.contains(&f.name))
366 .collect();
367 if !included_functions.is_empty() {
368 let facade_class_name = extension_name.to_pascal_case();
369 let mut method_items: Vec<String> = Vec::new();
372 for func in included_functions {
373 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
374 if let Some((param_idx, bridge_cfg)) = bridge_param {
375 let bridge_handle_path = bridge_handle_path(api, bridge_cfg, &core_import);
376 method_items.push(crate::trait_bridge::gen_bridge_function(
377 func,
378 param_idx,
379 bridge_cfg,
380 &mapper,
381 &opaque_types,
382 &core_import,
383 &bridge_handle_path,
384 ));
385 } else if func.is_async {
386 method_items.push(gen_async_function_as_static_method(
387 func,
388 &mapper,
389 &opaque_types,
390 &core_import,
391 &config.trait_bridges,
392 &mutex_types,
393 ));
394 } else {
395 method_items.push(gen_function_as_static_method(
396 func,
397 &mapper,
398 &opaque_types,
399 &core_import,
400 &config.trait_bridges,
401 has_serde,
402 &mutex_types,
403 ));
404 }
405 }
406
407 let methods_joined = method_items
408 .iter()
409 .map(|m| {
410 m.lines()
412 .map(|l| {
413 if l.is_empty() {
414 String::new()
415 } else {
416 format!(" {l}")
417 }
418 })
419 .collect::<Vec<_>>()
420 .join("\n")
421 })
422 .collect::<Vec<_>>()
423 .join("\n\n");
424 let php_api_class_name = format!("{facade_class_name}Api");
427 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
429 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
430 let facade_struct = format!(
431 "#[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}}"
432 );
433 builder.add_item(&facade_struct);
434
435 for bridge_cfg in &config.trait_bridges {
437 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
438 let bridge = crate::trait_bridge::gen_trait_bridge(
439 trait_type,
440 bridge_cfg,
441 &core_import,
442 &config.error_type_name(),
443 &config.error_constructor_expr(),
444 api,
445 );
446 for imp in &bridge.imports {
447 builder.add_import(imp);
448 }
449 builder.add_item(&bridge.code);
450 }
451 }
452 }
453
454 let convertible = alef_codegen::conversions::convertible_types(api);
455 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
456 let input_types = alef_codegen::conversions::input_type_names(api);
457 let enum_names_ref = &mapper.enum_names;
462 let bridge_skip_types: Vec<String> = config
463 .trait_bridges
464 .iter()
465 .filter(|b| !matches!(b.bind_via, alef_core::config::BridgeBinding::OptionsField))
466 .filter_map(|b| b.type_alias.clone())
467 .collect();
468 let trait_bridge_arc_wrapper_field_names: Vec<String> = config
473 .trait_bridges
474 .iter()
475 .filter(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField)
476 .filter_map(|b| b.resolved_options_field().map(String::from))
477 .collect();
478 let mut conv_opaque_types: AHashSet<String> = opaque_types.clone();
483 for bridge in &config.trait_bridges {
484 if let Some(alias) = &bridge.type_alias {
485 conv_opaque_types.insert(alias.clone());
486 }
487 }
488 let php_conv_config = ConversionConfig {
489 cast_large_ints_to_i64: true,
490 enum_string_names: Some(enum_names_ref),
491 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
492 json_as_value: true,
496 include_cfg_metadata: false,
497 option_duration_on_defaults: true,
498 from_binding_skip_types: &bridge_skip_types,
499 never_skip_cfg_field_names: &never_skip_cfg_field_names,
500 opaque_types: Some(&conv_opaque_types),
501 trait_bridge_arc_wrapper_field_names: &trait_bridge_arc_wrapper_field_names,
502 ..Default::default()
503 };
504 let mut enum_tainted: AHashSet<String> = AHashSet::new();
506 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
507 if has_enum_named_field(typ, enum_names_ref) {
508 enum_tainted.insert(typ.name.clone());
509 }
510 }
511 let mut changed = true;
513 while changed {
514 changed = false;
515 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
516 if !enum_tainted.contains(&typ.name)
517 && binding_fields(&typ.fields).any(|f| references_named_type(&f.ty, &enum_tainted))
518 {
519 enum_tainted.insert(typ.name.clone());
520 changed = true;
521 }
522 }
523 }
524 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
525 if input_types.contains(&typ.name)
527 && !enum_tainted.contains(&typ.name)
528 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
529 {
530 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
531 typ,
532 &core_import,
533 &php_conv_config,
534 ));
535 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
536 builder.add_item(&gen_enum_tainted_from_binding_to_core(
543 typ,
544 &core_import,
545 enum_names_ref,
546 &enum_tainted,
547 &php_conv_config,
548 &api.enums,
549 &bridge_type_aliases_set,
550 ));
551 }
552 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
554 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
555 typ,
556 &core_import,
557 &opaque_types,
558 &php_conv_config,
559 ));
560 }
561 }
562
563 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
565 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
566 }
567
568 for error in &api.errors {
570 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
571 }
572
573 if has_serde {
577 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
578 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
579 pub fn max_compression_ratio() -> i64 { 100 }\n\
580 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
581 pub fn max_nesting_depth() -> i64 { 1024 }\n\
582 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
583 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
584 pub fn max_iterations() -> i64 { 10_000_000 }\n\
585 pub fn max_xml_depth() -> i64 { 1024 }\n\
586 pub fn max_table_cells() -> i64 { 100_000 }\n\
587 }";
588 builder.add_item(serde_module);
589 }
590
591 let php_config = config.php.as_ref();
597 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
598
599 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
603 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
604 }
605
606 let mut class_registrations = String::new();
609 for typ in api
610 .types
611 .iter()
612 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
613 {
614 class_registrations.push_str(&crate::template_env::render(
615 "php_class_registration.jinja",
616 context! { class_name => &typ.name },
617 ));
618 }
619 if !api.functions.is_empty() {
621 let facade_class_name = extension_name.to_pascal_case();
622 class_registrations.push_str(&crate::template_env::render(
623 "php_class_registration.jinja",
624 context! { class_name => &format!("{facade_class_name}Api") },
625 ));
626 }
627 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
630 class_registrations.push_str(&crate::template_env::render(
631 "php_class_registration.jinja",
632 context! { class_name => &enum_def.name },
633 ));
634 }
635 builder.add_item(&format!(
636 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
637 ));
638
639 let mut content = builder.build();
640
641 for bridge in &config.trait_bridges {
646 if let Some(field_name) = bridge.resolved_options_field() {
647 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
648 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
649 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
650 let builder_type = format!("{}Builder", options_type);
651 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
652 let bridge_handle_path = bridge_handle_path(api, bridge, &core_import);
653
654 let old_method = format!(
660 " 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 }}"
661 );
662 let new_method = format!(
663 " 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 }}"
664 );
665
666 content = content.replace(&old_method, &new_method);
667 }
668 }
669
670 Ok(vec![GeneratedFile {
671 path: PathBuf::from(&output_dir).join("lib.rs"),
672 content,
673 generated_header: false,
674 }])
675 }
676
677 fn generate_public_api(
678 &self,
679 api: &ApiSurface,
680 config: &ResolvedCrateConfig,
681 ) -> anyhow::Result<Vec<GeneratedFile>> {
682 let extension_name = config.php_extension_name();
683 let class_name = extension_name.to_pascal_case();
684
685 let mut content = String::new();
687 content.push_str(&crate::template_env::render(
688 "php_file_header.jinja",
689 minijinja::Value::default(),
690 ));
691 content.push_str(&hash::header(CommentStyle::DoubleSlash));
692 content.push_str(&crate::template_env::render(
693 "php_declare_strict_types.jinja",
694 minijinja::Value::default(),
695 ));
696 content.push('\n');
698
699 let namespace = php_autoload_namespace(config);
701
702 content.push_str(&crate::template_env::render(
703 "php_namespace.jinja",
704 context! { namespace => &namespace },
705 ));
706 content.push('\n');
708 content.push_str(&crate::template_env::render(
709 "php_facade_class_declaration.jinja",
710 context! { class_name => &class_name },
711 ));
712
713 let bridge_param_names_pub: ahash::AHashSet<&str> = config
715 .trait_bridges
716 .iter()
717 .filter_map(|b| b.param_name.as_deref())
718 .collect();
719
720 let no_arg_constructor_types: AHashSet<String> = api
725 .types
726 .iter()
727 .filter(|t| t.fields.iter().all(|f| f.optional))
728 .map(|t| t.name.clone())
729 .collect();
730
731 for func in &api.functions {
733 let method_name = func.name.to_lower_camel_case();
738 let return_php_type = php_type(&func.return_type);
739
740 let visible_params: Vec<_> = func
742 .params
743 .iter()
744 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
745 .collect();
746
747 content.push_str(&crate::template_env::render(
749 "php_phpdoc_block_start.jinja",
750 minijinja::Value::default(),
751 ));
752 if func.doc.is_empty() {
753 content.push_str(&crate::template_env::render(
754 "php_phpdoc_text_line.jinja",
755 context! { text => &format!("{}.", method_name) },
756 ));
757 } else {
758 content.push_str(&crate::template_env::render(
759 "php_phpdoc_lines.jinja",
760 context! {
761 doc_lines => func.doc.lines().collect::<Vec<_>>(),
762 indent => " ",
763 },
764 ));
765 }
766 content.push_str(&crate::template_env::render(
767 "php_phpdoc_empty_line.jinja",
768 minijinja::Value::default(),
769 ));
770 for p in &visible_params {
771 let ptype = php_phpdoc_type(&p.ty);
772 let nullable_prefix = if p.optional { "?" } else { "" };
773 content.push_str(&crate::template_env::render(
774 "php_phpdoc_param_line.jinja",
775 context! {
776 nullable_prefix => nullable_prefix,
777 param_type => &ptype,
778 param_name => &p.name,
779 },
780 ));
781 }
782 let return_phpdoc = php_phpdoc_type(&func.return_type);
783 content.push_str(&crate::template_env::render(
784 "php_phpdoc_return_line.jinja",
785 context! { return_type => &return_phpdoc },
786 ));
787 if func.error_type.is_some() {
788 content.push_str(&crate::template_env::render(
789 "php_phpdoc_throws_line.jinja",
790 context! {
791 namespace => namespace.as_str(),
792 class_name => &class_name,
793 },
794 ));
795 }
796 content.push_str(&crate::template_env::render(
797 "php_phpdoc_block_end.jinja",
798 minijinja::Value::default(),
799 ));
800
801 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
812 if let TypeRef::Named(name) = &p.ty {
813 (name.ends_with("Config") || name.as_str() == "config")
814 && no_arg_constructor_types.contains(name.as_str())
815 } else {
816 false
817 }
818 };
819
820 let mut first_optional_idx = None;
821 for (idx, p) in visible_params.iter().enumerate() {
822 if p.optional || is_optional_config_param(p) {
823 first_optional_idx = Some(idx);
824 break;
825 }
826 }
827
828 content.push_str(&crate::template_env::render(
829 "php_method_signature_start.jinja",
830 context! { method_name => &method_name },
831 ));
832
833 let params: Vec<String> = visible_params
834 .iter()
835 .enumerate()
836 .map(|(idx, p)| {
837 let ptype = php_type(&p.ty);
838 let should_be_optional = p.optional
843 || is_optional_config_param(p)
844 || first_optional_idx.is_some_and(|first| idx >= first);
845 if should_be_optional {
846 format!("?{} ${} = null", ptype, p.name)
847 } else {
848 format!("{} ${}", ptype, p.name)
849 }
850 })
851 .collect();
852 content.push_str(¶ms.join(", "));
853 content.push_str(&crate::template_env::render(
854 "php_method_signature_end.jinja",
855 context! { return_type => &return_php_type },
856 ));
857 let ext_method_name = func.name.to_lower_camel_case();
862 let is_void = matches!(&func.return_type, TypeRef::Unit);
863 let call_params = visible_params
871 .iter()
872 .enumerate()
873 .map(|(idx, p)| {
874 let should_be_optional = p.optional
875 || is_optional_config_param(p)
876 || first_optional_idx.is_some_and(|first| idx >= first);
877 if should_be_optional && is_optional_config_param(p) {
878 if let TypeRef::Named(type_name) = &p.ty {
879 return format!("${} ?? new {}()", p.name, type_name);
880 }
881 }
882 format!("${}", p.name)
883 })
884 .collect::<Vec<_>>()
885 .join(", ");
886 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
887 if is_void {
888 content.push_str(&crate::template_env::render(
889 "php_method_call_statement.jinja",
890 context! { call_expr => &call_expr },
891 ));
892 } else {
893 content.push_str(&crate::template_env::render(
894 "php_method_call_return.jinja",
895 context! { call_expr => &call_expr },
896 ));
897 }
898 content.push_str(&crate::template_env::render(
899 "php_method_end.jinja",
900 minijinja::Value::default(),
901 ));
902 }
903
904 content.push_str(&crate::template_env::render(
905 "php_class_end.jinja",
906 minijinja::Value::default(),
907 ));
908
909 let output_dir = config
913 .php
914 .as_ref()
915 .and_then(|p| p.stubs.as_ref())
916 .map(|s| s.output.to_string_lossy().to_string())
917 .unwrap_or_else(|| "packages/php/src/".to_string());
918
919 let mut files: Vec<GeneratedFile> = Vec::new();
920 files.push(GeneratedFile {
921 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
922 content,
923 generated_header: false,
924 });
925
926 for typ in api.types.iter().filter(|t| t.is_opaque && !t.is_trait) {
932 let opaque_file = gen_php_opaque_class_file(typ, &namespace);
933 files.push(GeneratedFile {
934 path: PathBuf::from(&output_dir).join(format!("{}.php", typ.name)),
935 content: opaque_file,
936 generated_header: false,
937 });
938 }
939
940 Ok(files)
941 }
942
943 fn generate_type_stubs(
944 &self,
945 api: &ApiSurface,
946 config: &ResolvedCrateConfig,
947 ) -> anyhow::Result<Vec<GeneratedFile>> {
948 let extension_name = config.php_extension_name();
949 let class_name = extension_name.to_pascal_case();
950
951 let namespace = php_autoload_namespace(config);
953
954 let mut content = String::new();
959 content.push_str(&crate::template_env::render(
960 "php_file_header.jinja",
961 minijinja::Value::default(),
962 ));
963 content.push_str(&hash::header(CommentStyle::DoubleSlash));
964 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
965 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
966 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
967 content.push_str(&crate::template_env::render(
968 "php_declare_strict_types.jinja",
969 minijinja::Value::default(),
970 ));
971 content.push('\n');
973 content.push_str(&crate::template_env::render(
975 "php_namespace_block_begin.jinja",
976 context! { namespace => &namespace },
977 ));
978
979 content.push_str(&crate::template_env::render(
981 "php_exception_class_declaration.jinja",
982 context! { class_name => &class_name },
983 ));
984 content.push_str(
985 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
986 );
987 content.push_str("}\n\n");
988
989 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
996 if typ.is_opaque || typ.fields.is_empty() {
997 continue;
998 }
999 if !typ.doc.is_empty() {
1000 content.push_str("/**\n");
1001 content.push_str(&crate::template_env::render(
1002 "php_phpdoc_lines.jinja",
1003 context! {
1004 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
1005 indent => "",
1006 },
1007 ));
1008 content.push_str(" */\n");
1009 }
1010 content.push_str(&crate::template_env::render(
1011 "php_record_class_stub_declaration.jinja",
1012 context! { class_name => &typ.name },
1013 ));
1014
1015 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = binding_fields(&typ.fields).collect();
1018 sorted_fields.sort_by_key(|f| f.optional);
1019
1020 let params: Vec<String> = sorted_fields
1025 .iter()
1026 .map(|f| {
1027 let ptype = php_type(&f.ty);
1028 let nullable = if f.optional && !ptype.starts_with('?') {
1029 format!("?{ptype}")
1030 } else {
1031 ptype
1032 };
1033 let default = if f.optional { " = null" } else { "" };
1034 let php_name = to_php_name(&f.name);
1035 let phpdoc_type = php_phpdoc_type(&f.ty);
1036 let var_type = if f.optional && !phpdoc_type.starts_with('?') {
1037 format!("?{phpdoc_type}")
1038 } else {
1039 phpdoc_type
1040 };
1041 let phpdoc = php_property_phpdoc(&var_type, &f.doc, " ");
1042 format!("{phpdoc} public readonly {nullable} ${php_name}{default}",)
1043 })
1044 .collect();
1045 content.push_str(&crate::template_env::render(
1046 "php_constructor_method.jinja",
1047 context! { params => ¶ms.join(",\n") },
1048 ));
1049
1050 content.push_str("}\n\n");
1051 }
1052
1053 for enum_def in &api.enums {
1056 if is_tagged_data_enum(enum_def) {
1057 if !enum_def.doc.is_empty() {
1059 content.push_str("/**\n");
1060 content.push_str(&crate::template_env::render(
1061 "php_phpdoc_lines.jinja",
1062 context! {
1063 doc_lines => enum_def.doc.lines().collect::<Vec<_>>(),
1064 indent => "",
1065 },
1066 ));
1067 content.push_str(" */\n");
1068 }
1069 content.push_str(&crate::template_env::render(
1070 "php_record_class_stub_declaration.jinja",
1071 context! { class_name => &enum_def.name },
1072 ));
1073 content.push_str("}\n\n");
1074 } else {
1075 content.push_str(&crate::template_env::render(
1077 "php_tagged_enum_declaration.jinja",
1078 context! { enum_name => &enum_def.name },
1079 ));
1080 for variant in &enum_def.variants {
1081 let case_name = sanitize_php_enum_case(&variant.name);
1082 content.push_str(&crate::template_env::render(
1083 "php_enum_variant_stub.jinja",
1084 context! {
1085 variant_name => case_name,
1086 value => &variant.name,
1087 },
1088 ));
1089 }
1090 content.push_str("}\n\n");
1091 }
1092 }
1093
1094 if !api.functions.is_empty() {
1099 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1101 .trait_bridges
1102 .iter()
1103 .filter_map(|b| b.param_name.as_deref())
1104 .collect();
1105
1106 content.push_str(&crate::template_env::render(
1107 "php_api_class_declaration.jinja",
1108 context! { class_name => &class_name },
1109 ));
1110 for func in &api.functions {
1111 let return_type = php_type_fq(&func.return_type, &namespace);
1112 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1113 let visible_params: Vec<_> = func
1115 .params
1116 .iter()
1117 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1118 .collect();
1119 let has_array_params = visible_params
1126 .iter()
1127 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1128 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1129 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1130 let first_optional_idx = visible_params.iter().position(|p| p.optional);
1131 if has_array_params || has_array_return {
1132 content.push_str(" /**\n");
1133 for (idx, p) in visible_params.iter().enumerate() {
1134 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1135 let nullable_prefix = if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1136 "?"
1137 } else {
1138 ""
1139 };
1140 content.push_str(&crate::template_env::render(
1141 "php_phpdoc_static_param.jinja",
1142 context! {
1143 nullable_prefix => nullable_prefix,
1144 ptype => &ptype,
1145 param_name => &p.name,
1146 },
1147 ));
1148 }
1149 content.push_str(&crate::template_env::render(
1150 "php_phpdoc_static_return.jinja",
1151 context! { return_phpdoc => &return_phpdoc },
1152 ));
1153 content.push_str(" */\n");
1154 }
1155 let params: Vec<String> = visible_params
1156 .iter()
1157 .enumerate()
1158 .map(|(idx, p)| {
1159 let ptype = php_type_fq(&p.ty, &namespace);
1160 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1161 let nullable_ptype = if ptype.starts_with('?') {
1162 ptype
1163 } else {
1164 format!("?{ptype}")
1165 };
1166 format!("{} ${} = null", nullable_ptype, p.name)
1167 } else {
1168 format!("{} ${}", ptype, p.name)
1169 }
1170 })
1171 .collect();
1172 let stub_method_name = func.name.to_lower_camel_case();
1176 let is_void_stub = return_type == "void";
1177 let stub_body = if is_void_stub {
1178 "{ }".to_string()
1179 } else {
1180 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1181 };
1182 content.push_str(&crate::template_env::render(
1183 "php_static_method_stub.jinja",
1184 context! {
1185 method_name => &stub_method_name,
1186 params => ¶ms.join(", "),
1187 return_type => &return_type,
1188 stub_body => &stub_body,
1189 },
1190 ));
1191 }
1192 content.push_str("}\n\n");
1193 }
1194
1195 content.push_str(&crate::template_env::render(
1197 "php_namespace_block_end.jinja",
1198 minijinja::Value::default(),
1199 ));
1200
1201 let output_dir = config
1203 .php
1204 .as_ref()
1205 .and_then(|p| p.stubs.as_ref())
1206 .map(|s| s.output.to_string_lossy().to_string())
1207 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1208
1209 Ok(vec![GeneratedFile {
1210 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1211 content,
1212 generated_header: false,
1213 }])
1214 }
1215
1216 fn build_config(&self) -> Option<BuildConfig> {
1217 Some(BuildConfig {
1218 tool: "cargo",
1219 crate_suffix: "-php",
1220 build_dep: BuildDependency::None,
1221 post_build: vec![],
1222 })
1223 }
1224}
1225
1226fn bridge_handle_path(api: &ApiSurface, bridge: &alef_core::config::TraitBridgeConfig, core_import: &str) -> String {
1227 let alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
1228 api.types
1229 .iter()
1230 .find(|t| t.name == alias && !t.rust_path.is_empty())
1231 .map(|t| t.rust_path.replace('-', "_"))
1232 .or_else(|| api.excluded_type_paths.get(alias).map(|path| path.replace('-', "_")))
1233 .unwrap_or_else(|| format!("{core_import}::visitor::{alias}"))
1234}
1235
1236fn php_phpdoc_type(ty: &TypeRef) -> String {
1239 match ty {
1240 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1241 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1242 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1243 _ => php_type(ty),
1244 }
1245}
1246
1247fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1249 match ty {
1250 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1251 TypeRef::Map(k, v) => format!(
1252 "array<{}, {}>",
1253 php_phpdoc_type_fq(k, namespace),
1254 php_phpdoc_type_fq(v, namespace)
1255 ),
1256 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1257 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1258 _ => php_type(ty),
1259 }
1260}
1261
1262fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1264 match ty {
1265 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1266 TypeRef::Optional(inner) => {
1267 let inner_type = php_type_fq(inner, namespace);
1268 if inner_type.starts_with('?') {
1269 inner_type
1270 } else {
1271 format!("?{inner_type}")
1272 }
1273 }
1274 _ => php_type(ty),
1275 }
1276}
1277
1278fn gen_php_opaque_class_file(typ: &alef_core::ir::TypeDef, namespace: &str) -> String {
1285 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(&crate::template_env::render(
1292 "php_declare_strict_types.jinja",
1293 minijinja::Value::default(),
1294 ));
1295 content.push('\n');
1297 content.push_str(&crate::template_env::render(
1298 "php_namespace.jinja",
1299 context! { namespace => namespace },
1300 ));
1301 content.push('\n');
1303
1304 if !typ.doc.is_empty() {
1306 content.push_str("/**\n");
1307 content.push_str(&crate::template_env::render(
1308 "php_phpdoc_lines.jinja",
1309 context! {
1310 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
1311 indent => "",
1312 },
1313 ));
1314 content.push_str(" */\n");
1315 }
1316
1317 content.push_str(&format!("final class {}\n{{\n", typ.name));
1318
1319 let mut method_order: Vec<&alef_core::ir::MethodDef> = Vec::new();
1321 method_order.extend(typ.methods.iter().filter(|m| m.receiver.is_some()));
1322 method_order.extend(typ.methods.iter().filter(|m| m.receiver.is_none()));
1323
1324 for method in method_order {
1325 let method_name = method.name.to_lower_camel_case();
1326 let return_type = php_type(&method.return_type);
1327 let is_void = matches!(&method.return_type, TypeRef::Unit);
1328 let is_static = method.receiver.is_none();
1329
1330 let mut doc_lines: Vec<String> = vec![];
1332 let doc_line = method.doc.lines().next().unwrap_or("").trim();
1333 if !doc_line.is_empty() {
1334 doc_lines.push(doc_line.to_string());
1335 }
1336
1337 let mut phpdoc_params: Vec<String> = vec![];
1339 for param in &method.params {
1340 if matches!(¶m.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)) {
1341 let phpdoc_type = php_phpdoc_type(¶m.ty);
1342 phpdoc_params.push(format!("@param {} ${}", phpdoc_type, param.name));
1343 }
1344 }
1345 doc_lines.extend(phpdoc_params);
1346
1347 let needs_return_phpdoc = matches!(&method.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _));
1349 if needs_return_phpdoc {
1350 let phpdoc_type = php_phpdoc_type(&method.return_type);
1351 doc_lines.push(format!("@return {phpdoc_type}"));
1352 }
1353
1354 if !doc_lines.is_empty() {
1356 content.push_str(" /**\n");
1357 for line in doc_lines {
1358 content.push_str(&format!(" * {}\n", line));
1359 }
1360 content.push_str(" */\n");
1361 }
1362
1363 let static_kw = if is_static { "static " } else { "" };
1365 let first_optional_idx = method.params.iter().position(|p| p.optional);
1366 let params: Vec<String> = method
1367 .params
1368 .iter()
1369 .enumerate()
1370 .map(|(idx, p)| {
1371 let ptype = php_type(&p.ty);
1372 if p.optional || first_optional_idx.is_some_and(|first| idx >= first) {
1373 let nullable = if ptype.starts_with('?') { "" } else { "?" };
1374 format!("{nullable}{ptype} ${} = null", p.name)
1375 } else {
1376 format!("{} ${}", ptype, p.name)
1377 }
1378 })
1379 .collect();
1380 content.push_str(&format!(
1381 " public {static_kw}function {method_name}({}): {return_type}\n",
1382 params.join(", ")
1383 ));
1384 let body = if is_void {
1385 " {\n }\n"
1386 } else {
1387 " {\n throw new \\RuntimeException('Not implemented — provided by the native extension.');\n }\n"
1388 };
1389 content.push_str(body);
1390 }
1391
1392 content.push_str("}\n");
1393 content
1394}
1395
1396fn php_type(ty: &TypeRef) -> String {
1398 match ty {
1399 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1400 TypeRef::Primitive(p) => match p {
1401 PrimitiveType::Bool => "bool".to_string(),
1402 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1403 PrimitiveType::U8
1404 | PrimitiveType::U16
1405 | PrimitiveType::U32
1406 | PrimitiveType::U64
1407 | PrimitiveType::I8
1408 | PrimitiveType::I16
1409 | PrimitiveType::I32
1410 | PrimitiveType::I64
1411 | PrimitiveType::Usize
1412 | PrimitiveType::Isize => "int".to_string(),
1413 },
1414 TypeRef::Optional(inner) => {
1415 let inner_type = php_type(inner);
1418 if inner_type.starts_with('?') {
1419 inner_type
1420 } else {
1421 format!("?{inner_type}")
1422 }
1423 }
1424 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1425 TypeRef::Named(name) => name.clone(),
1426 TypeRef::Unit => "void".to_string(),
1427 TypeRef::Duration => "float".to_string(),
1428 }
1429}
1430
1431fn php_property_phpdoc(var_type: &str, doc: &str, indent: &str) -> String {
1440 let doc = doc.trim();
1441 if doc.is_empty() {
1442 return format!("{indent}/** @var {var_type} */\n");
1443 }
1444 let lines: Vec<&str> = doc.lines().collect();
1445 if lines.len() == 1 {
1446 let line = lines[0].trim();
1447 return format!("{indent}/** @var {var_type} {line} */\n");
1448 }
1449 let mut out = format!("{indent}/**\n");
1451 for line in &lines {
1452 let trimmed = line.trim();
1453 if trimmed.is_empty() {
1454 out.push_str(&format!("{indent} *\n"));
1455 } else {
1456 out.push_str(&format!("{indent} * {trimmed}\n"));
1457 }
1458 }
1459 out.push_str(&format!("{indent} *\n"));
1460 out.push_str(&format!("{indent} * @var {var_type}\n"));
1461 out.push_str(&format!("{indent} */\n"));
1462 out
1463}