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 }
68 }
69}
70
71impl Backend for PhpBackend {
72 fn name(&self) -> &str {
73 "php"
74 }
75
76 fn language(&self) -> Language {
77 Language::Php
78 }
79
80 fn capabilities(&self) -> Capabilities {
81 Capabilities {
82 supports_async: false,
83 supports_classes: true,
84 supports_enums: true,
85 supports_option: true,
86 supports_result: true,
87 ..Capabilities::default()
88 }
89 }
90
91 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
92 let data_enum_names: AHashSet<String> = api
95 .enums
96 .iter()
97 .filter(|e| is_tagged_data_enum(e))
98 .map(|e| e.name.clone())
99 .collect();
100 let untagged_data_enum_names: AHashSet<String> = api
101 .enums
102 .iter()
103 .filter(|e| is_untagged_data_enum(e))
104 .map(|e| e.name.clone())
105 .collect();
106 let enum_names: AHashSet<String> = api
109 .enums
110 .iter()
111 .filter(|e| !is_tagged_data_enum(e) && !is_untagged_data_enum(e))
112 .map(|e| e.name.clone())
113 .collect();
114 let mapper = PhpMapper {
115 enum_names: enum_names.clone(),
116 data_enum_names: data_enum_names.clone(),
117 untagged_data_enum_names: untagged_data_enum_names.clone(),
118 };
119 let core_import = config.core_import_name();
120
121 let php_config = config.php.as_ref();
123 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
124 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
125
126 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
127 let has_serde = detect_serde_available(&output_dir);
128
129 let bridge_type_aliases_php: Vec<String> = config
135 .trait_bridges
136 .iter()
137 .filter_map(|b| b.type_alias.clone())
138 .collect();
139 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
140 let mut opaque_names_vec_php: Vec<String> = api
141 .types
142 .iter()
143 .filter(|t| t.is_opaque)
144 .map(|t| t.name.clone())
145 .collect();
146 opaque_names_vec_php.extend(bridge_type_aliases_php);
147
148 let mut cfg = Self::binding_config(&core_import, has_serde);
149 cfg.opaque_type_names = &opaque_names_vec_php;
150
151 let mut builder = RustFileBuilder::new().with_generated_header();
153 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
154 builder.add_inner_attribute("allow(unsafe_code)");
155 builder.add_inner_attribute("allow(non_snake_case)");
157 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)");
158 builder.add_import("ext_php_rs::prelude::*");
159
160 if has_serde {
162 builder.add_import("serde_json");
163 }
164
165 for trait_path in generators::collect_trait_imports(api) {
167 builder.add_import(&trait_path);
168 }
169
170 let has_maps = api.types.iter().any(|t| {
172 t.fields
173 .iter()
174 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
175 }) || api
176 .functions
177 .iter()
178 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
179 if has_maps {
180 builder.add_import("std::collections::HashMap");
181 }
182
183 builder.add_item(
188 "#[derive(Debug, Clone, Default)]\n\
189 pub struct PhpBytes(pub Vec<u8>);\n\
190 \n\
191 impl<'a> ext_php_rs::convert::FromZval<'a> for PhpBytes {\n \
192 const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::String;\n \
193 fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {\n \
194 zval.zend_str().map(|zs| PhpBytes(zs.as_bytes().to_vec()))\n \
195 }\n\
196 }\n\
197 \n\
198 impl From<PhpBytes> for Vec<u8> {\n \
199 fn from(b: PhpBytes) -> Self { b.0 }\n\
200 }\n\
201 \n\
202 impl From<Vec<u8>> for PhpBytes {\n \
203 fn from(v: Vec<u8>) -> Self { PhpBytes(v) }\n\
204 }\n",
205 );
206
207 let custom_mods = config.custom_modules.for_language(Language::Php);
209 for module in custom_mods {
210 builder.add_item(&format!("pub mod {module};"));
211 }
212
213 let has_async =
215 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
216
217 if has_async {
218 builder.add_item(&gen_tokio_runtime());
219 }
220
221 let opaque_types: AHashSet<String> = api
223 .types
224 .iter()
225 .filter(|t| t.is_opaque)
226 .map(|t| t.name.clone())
227 .collect();
228 if !opaque_types.is_empty() {
229 builder.add_import("std::sync::Arc");
230 }
231
232 let extension_name = config.php_extension_name();
235 let php_namespace = php_autoload_namespace(config);
236
237 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
239
240 for adapter in &config.adapters {
242 match adapter.pattern {
243 alef_core::config::AdapterPattern::Streaming => {
244 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
245 if let Some(struct_code) = adapter_bodies.get(&key) {
246 builder.add_item(struct_code);
247 }
248 }
249 alef_core::config::AdapterPattern::CallbackBridge => {
250 let struct_key = format!("{}.__bridge_struct__", adapter.name);
251 let impl_key = format!("{}.__bridge_impl__", adapter.name);
252 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
253 builder.add_item(struct_code);
254 }
255 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
256 builder.add_item(impl_code);
257 }
258 }
259 _ => {}
260 }
261 }
262
263 for typ in api
264 .types
265 .iter()
266 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
267 {
268 if typ.is_opaque {
269 let ns_escaped = php_namespace.replace('\\', "\\\\");
273 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
274 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
275 let opaque_cfg = RustBindingConfig {
276 struct_attrs: &opaque_attr_arr,
277 ..cfg
278 };
279 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
280 builder.add_item(&gen_opaque_struct_methods(
281 typ,
282 &mapper,
283 &opaque_types,
284 &core_import,
285 &adapter_bodies,
286 ));
287 } else {
288 builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace), &enum_names));
291 builder.add_item(&types::gen_struct_methods_with_exclude(
292 typ,
293 &mapper,
294 has_serde,
295 &core_import,
296 &opaque_types,
297 &enum_names,
298 &api.enums,
299 &exclude_functions,
300 &bridge_type_aliases_set,
301 ));
302 }
303 }
304
305 for enum_def in &api.enums {
306 if is_tagged_data_enum(enum_def) {
307 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
309 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
310 } else {
311 builder.add_item(&gen_enum_constants(enum_def));
312 }
313 }
314
315 let included_functions: Vec<_> = api
320 .functions
321 .iter()
322 .filter(|f| !exclude_functions.contains(&f.name))
323 .collect();
324 if !included_functions.is_empty() {
325 let facade_class_name = extension_name.to_pascal_case();
326 let mut method_items: Vec<String> = Vec::new();
329 for func in included_functions {
330 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
331 if let Some((param_idx, bridge_cfg)) = bridge_param {
332 method_items.push(crate::trait_bridge::gen_bridge_function(
333 func,
334 param_idx,
335 bridge_cfg,
336 &mapper,
337 &opaque_types,
338 &core_import,
339 ));
340 } else if func.is_async {
341 method_items.push(gen_async_function_as_static_method(
342 func,
343 &mapper,
344 &opaque_types,
345 &core_import,
346 &config.trait_bridges,
347 ));
348 } else {
349 method_items.push(gen_function_as_static_method(
350 func,
351 &mapper,
352 &opaque_types,
353 &core_import,
354 &config.trait_bridges,
355 has_serde,
356 ));
357 }
358 }
359
360 let methods_joined = method_items
361 .iter()
362 .map(|m| {
363 m.lines()
365 .map(|l| {
366 if l.is_empty() {
367 String::new()
368 } else {
369 format!(" {l}")
370 }
371 })
372 .collect::<Vec<_>>()
373 .join("\n")
374 })
375 .collect::<Vec<_>>()
376 .join("\n\n");
377 let php_api_class_name = format!("{facade_class_name}Api");
380 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
382 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
383 let facade_struct = format!(
384 "#[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}}"
385 );
386 builder.add_item(&facade_struct);
387
388 for bridge_cfg in &config.trait_bridges {
390 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
391 let bridge = crate::trait_bridge::gen_trait_bridge(
392 trait_type,
393 bridge_cfg,
394 &core_import,
395 &config.error_type_name(),
396 &config.error_constructor_expr(),
397 api,
398 );
399 for imp in &bridge.imports {
400 builder.add_import(imp);
401 }
402 builder.add_item(&bridge.code);
403 }
404 }
405 }
406
407 let convertible = alef_codegen::conversions::convertible_types(api);
408 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
409 let input_types = alef_codegen::conversions::input_type_names(api);
410 let enum_names_ref = &mapper.enum_names;
415 let bridge_skip_types: Vec<String> = config
416 .trait_bridges
417 .iter()
418 .filter_map(|b| b.type_alias.clone())
419 .collect();
420 let php_conv_config = ConversionConfig {
421 cast_large_ints_to_i64: true,
422 enum_string_names: Some(enum_names_ref),
423 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
424 json_as_value: true,
428 include_cfg_metadata: false,
429 option_duration_on_defaults: true,
430 from_binding_skip_types: &bridge_skip_types,
431 ..Default::default()
432 };
433 let mut enum_tainted: AHashSet<String> = AHashSet::new();
435 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
436 if has_enum_named_field(typ, enum_names_ref) {
437 enum_tainted.insert(typ.name.clone());
438 }
439 }
440 let mut changed = true;
442 while changed {
443 changed = false;
444 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
445 if !enum_tainted.contains(&typ.name)
446 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
447 {
448 enum_tainted.insert(typ.name.clone());
449 changed = true;
450 }
451 }
452 }
453 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
454 if input_types.contains(&typ.name)
456 && !enum_tainted.contains(&typ.name)
457 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
458 {
459 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
460 typ,
461 &core_import,
462 &php_conv_config,
463 ));
464 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
465 builder.add_item(&gen_enum_tainted_from_binding_to_core(
472 typ,
473 &core_import,
474 enum_names_ref,
475 &enum_tainted,
476 &php_conv_config,
477 &api.enums,
478 &bridge_type_aliases_set,
479 ));
480 }
481 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
483 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
484 typ,
485 &core_import,
486 &opaque_types,
487 &php_conv_config,
488 ));
489 }
490 }
491
492 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
494 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
495 }
496
497 for error in &api.errors {
499 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
500 }
501
502 if has_serde {
506 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
507 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
508 pub fn max_compression_ratio() -> i64 { 100 }\n\
509 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
510 pub fn max_nesting_depth() -> i64 { 1024 }\n\
511 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
512 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
513 pub fn max_iterations() -> i64 { 10_000_000 }\n\
514 pub fn max_xml_depth() -> i64 { 1024 }\n\
515 pub fn max_table_cells() -> i64 { 100_000 }\n\
516 }";
517 builder.add_item(serde_module);
518 }
519
520 let php_config = config.php.as_ref();
526 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
527
528 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
532 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
533 }
534
535 let mut class_registrations = String::new();
538 for typ in api
539 .types
540 .iter()
541 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
542 {
543 class_registrations.push_str(&crate::template_env::render(
544 "php_class_registration.jinja",
545 context! { class_name => &typ.name },
546 ));
547 }
548 if !api.functions.is_empty() {
550 let facade_class_name = extension_name.to_pascal_case();
551 class_registrations.push_str(&crate::template_env::render(
552 "php_class_registration.jinja",
553 context! { class_name => &format!("{facade_class_name}Api") },
554 ));
555 }
556 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
559 class_registrations.push_str(&crate::template_env::render(
560 "php_class_registration.jinja",
561 context! { class_name => &enum_def.name },
562 ));
563 }
564 builder.add_item(&format!(
565 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
566 ));
567
568 let mut content = builder.build();
569
570 for bridge in &config.trait_bridges {
575 if let Some(field_name) = bridge.resolved_options_field() {
576 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
577 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
578 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
579 let builder_type = format!("{}Builder", options_type);
580 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
581
582 let old_method = format!(
588 " 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 }}"
589 );
590 let new_method = format!(
591 " 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: html_to_markdown_rs::visitor::VisitorHandle = std::rc::Rc::new(std::cell::RefCell::new(bridge));\n Self {{ inner: Arc::new((*self.inner).clone().{field_name}(Some(handle))) }}\n }}"
592 );
593
594 content = content.replace(&old_method, &new_method);
595 }
596 }
597
598 Ok(vec![GeneratedFile {
599 path: PathBuf::from(&output_dir).join("lib.rs"),
600 content,
601 generated_header: false,
602 }])
603 }
604
605 fn generate_public_api(
606 &self,
607 api: &ApiSurface,
608 config: &ResolvedCrateConfig,
609 ) -> anyhow::Result<Vec<GeneratedFile>> {
610 let extension_name = config.php_extension_name();
611 let class_name = extension_name.to_pascal_case();
612
613 let mut content = String::new();
615 content.push_str(&crate::template_env::render(
616 "php_file_header.jinja",
617 minijinja::Value::default(),
618 ));
619 content.push_str(&hash::header(CommentStyle::DoubleSlash));
620 content.push_str(&crate::template_env::render(
621 "php_declare_strict_types.jinja",
622 minijinja::Value::default(),
623 ));
624
625 let namespace = php_autoload_namespace(config);
627
628 content.push_str(&crate::template_env::render(
629 "php_namespace.jinja",
630 context! { namespace => &namespace },
631 ));
632 content.push_str(&crate::template_env::render(
633 "php_facade_class_declaration.jinja",
634 context! { class_name => &class_name },
635 ));
636
637 let bridge_param_names_pub: ahash::AHashSet<&str> = config
639 .trait_bridges
640 .iter()
641 .filter_map(|b| b.param_name.as_deref())
642 .collect();
643
644 let no_arg_constructor_types: AHashSet<String> = api
649 .types
650 .iter()
651 .filter(|t| t.fields.iter().all(|f| f.optional))
652 .map(|t| t.name.clone())
653 .collect();
654
655 for func in &api.functions {
657 let method_name = func.name.to_lower_camel_case();
662 let return_php_type = php_type(&func.return_type);
663
664 let visible_params: Vec<_> = func
666 .params
667 .iter()
668 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
669 .collect();
670
671 content.push_str(&crate::template_env::render(
673 "php_phpdoc_block_start.jinja",
674 minijinja::Value::default(),
675 ));
676 if func.doc.is_empty() {
677 content.push_str(&crate::template_env::render(
678 "php_phpdoc_text_line.jinja",
679 context! { text => &format!("{}.", method_name) },
680 ));
681 } else {
682 content.push_str(&crate::template_env::render(
683 "php_phpdoc_lines.jinja",
684 context! {
685 doc_lines => func.doc.lines().collect::<Vec<_>>(),
686 indent => " ",
687 },
688 ));
689 }
690 content.push_str(&crate::template_env::render(
691 "php_phpdoc_empty_line.jinja",
692 minijinja::Value::default(),
693 ));
694 for p in &visible_params {
695 let ptype = php_phpdoc_type(&p.ty);
696 let nullable_prefix = if p.optional { "?" } else { "" };
697 content.push_str(&crate::template_env::render(
698 "php_phpdoc_param_line.jinja",
699 context! {
700 nullable_prefix => nullable_prefix,
701 param_type => &ptype,
702 param_name => &p.name,
703 },
704 ));
705 }
706 let return_phpdoc = php_phpdoc_type(&func.return_type);
707 content.push_str(&crate::template_env::render(
708 "php_phpdoc_return_line.jinja",
709 context! { return_type => &return_phpdoc },
710 ));
711 if func.error_type.is_some() {
712 content.push_str(&crate::template_env::render(
713 "php_phpdoc_throws_line.jinja",
714 context! {
715 namespace => namespace.as_str(),
716 class_name => &class_name,
717 },
718 ));
719 }
720 content.push_str(&crate::template_env::render(
721 "php_phpdoc_block_end.jinja",
722 minijinja::Value::default(),
723 ));
724
725 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
736 if let TypeRef::Named(name) = &p.ty {
737 (name.ends_with("Config") || name.as_str() == "config")
738 && no_arg_constructor_types.contains(name.as_str())
739 } else {
740 false
741 }
742 };
743
744 let mut first_optional_idx = None;
745 for (idx, p) in visible_params.iter().enumerate() {
746 if p.optional || is_optional_config_param(p) {
747 first_optional_idx = Some(idx);
748 break;
749 }
750 }
751
752 content.push_str(&crate::template_env::render(
753 "php_method_signature_start.jinja",
754 context! { method_name => &method_name },
755 ));
756
757 let params: Vec<String> = visible_params
758 .iter()
759 .enumerate()
760 .map(|(idx, p)| {
761 let ptype = php_type(&p.ty);
762 let should_be_optional = p.optional
767 || is_optional_config_param(p)
768 || first_optional_idx.is_some_and(|first| idx >= first);
769 if should_be_optional {
770 format!("?{} ${} = null", ptype, p.name)
771 } else {
772 format!("{} ${}", ptype, p.name)
773 }
774 })
775 .collect();
776 content.push_str(¶ms.join(", "));
777 content.push_str(&crate::template_env::render(
778 "php_method_signature_end.jinja",
779 context! { return_type => &return_php_type },
780 ));
781 let ext_method_name = if func.is_async {
786 format!("{}_async", func.name).to_lower_camel_case()
787 } else {
788 func.name.to_lower_camel_case()
789 };
790 let is_void = matches!(&func.return_type, TypeRef::Unit);
791 let call_params = visible_params
799 .iter()
800 .enumerate()
801 .map(|(idx, p)| {
802 let should_be_optional = p.optional
803 || is_optional_config_param(p)
804 || first_optional_idx.is_some_and(|first| idx >= first);
805 if should_be_optional && is_optional_config_param(p) {
806 if let TypeRef::Named(type_name) = &p.ty {
807 return format!("${} ?? new {}()", p.name, type_name);
808 }
809 }
810 format!("${}", p.name)
811 })
812 .collect::<Vec<_>>()
813 .join(", ");
814 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
815 if is_void {
816 content.push_str(&crate::template_env::render(
817 "php_method_call_statement.jinja",
818 context! { call_expr => &call_expr },
819 ));
820 } else {
821 content.push_str(&crate::template_env::render(
822 "php_method_call_return.jinja",
823 context! { call_expr => &call_expr },
824 ));
825 }
826 content.push_str(&crate::template_env::render(
827 "php_method_end.jinja",
828 minijinja::Value::default(),
829 ));
830 }
831
832 content.push_str(&crate::template_env::render(
833 "php_class_end.jinja",
834 minijinja::Value::default(),
835 ));
836
837 let output_dir = config
841 .php
842 .as_ref()
843 .and_then(|p| p.stubs.as_ref())
844 .map(|s| s.output.to_string_lossy().to_string())
845 .unwrap_or_else(|| "packages/php/src/".to_string());
846
847 Ok(vec![GeneratedFile {
848 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
849 content,
850 generated_header: false,
851 }])
852 }
853
854 fn generate_type_stubs(
855 &self,
856 api: &ApiSurface,
857 config: &ResolvedCrateConfig,
858 ) -> anyhow::Result<Vec<GeneratedFile>> {
859 let extension_name = config.php_extension_name();
860 let class_name = extension_name.to_pascal_case();
861
862 let namespace = php_autoload_namespace(config);
864
865 let mut content = String::new();
870 content.push_str(&crate::template_env::render(
871 "php_file_header.jinja",
872 minijinja::Value::default(),
873 ));
874 content.push_str(&hash::header(CommentStyle::DoubleSlash));
875 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
876 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
877 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
878 content.push_str(&crate::template_env::render(
879 "php_declare_strict_types.jinja",
880 minijinja::Value::default(),
881 ));
882 content.push_str(&crate::template_env::render(
884 "php_namespace_block_begin.jinja",
885 context! { namespace => &namespace },
886 ));
887
888 content.push_str(&crate::template_env::render(
890 "php_exception_class_declaration.jinja",
891 context! { class_name => &class_name },
892 ));
893 content.push_str(
894 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
895 );
896 content.push_str("}\n\n");
897
898 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
900 if typ.is_opaque {
901 if !typ.doc.is_empty() {
902 content.push_str("/**\n");
903 content.push_str(&crate::template_env::render(
904 "php_phpdoc_lines.jinja",
905 context! {
906 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
907 indent => "",
908 },
909 ));
910 content.push_str(" */\n");
911 }
912 content.push_str(&crate::template_env::render(
913 "php_opaque_class_stub_declaration.jinja",
914 context! { class_name => &typ.name },
915 ));
916 content.push_str("}\n\n");
918 }
919 }
920
921 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
923 if typ.is_opaque || typ.fields.is_empty() {
924 continue;
925 }
926 if !typ.doc.is_empty() {
927 content.push_str("/**\n");
928 content.push_str(&crate::template_env::render(
929 "php_phpdoc_lines.jinja",
930 context! {
931 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
932 indent => "",
933 },
934 ));
935 content.push_str(" */\n");
936 }
937 content.push_str(&crate::template_env::render(
938 "php_record_class_stub_declaration.jinja",
939 context! { class_name => &typ.name },
940 ));
941
942 for field in &typ.fields {
944 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
945 let prop_type = if field.optional {
946 let inner = php_type(&field.ty);
947 if inner.starts_with('?') {
948 inner
949 } else {
950 format!("?{inner}")
951 }
952 } else {
953 php_type(&field.ty)
954 };
955 if is_array {
956 let phpdoc = php_phpdoc_type(&field.ty);
957 let nullable_prefix = if field.optional { "?" } else { "" };
958 content.push_str(&crate::template_env::render(
959 "php_property_type_annotation.jinja",
960 context! {
961 nullable_prefix => nullable_prefix,
962 phpdoc => &phpdoc,
963 },
964 ));
965 }
966 content.push_str(&crate::template_env::render(
967 "php_property_stub.jinja",
968 context! {
969 prop_type => &prop_type,
970 field_name => &field.name,
971 },
972 ));
973 }
974 content.push('\n');
975
976 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
980 sorted_fields.sort_by_key(|f| f.optional);
981
982 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
985 .iter()
986 .copied()
987 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
988 .collect();
989 if !array_fields.is_empty() {
990 content.push_str(" /**\n");
991 for f in &array_fields {
992 let phpdoc = php_phpdoc_type(&f.ty);
993 let nullable_prefix = if f.optional { "?" } else { "" };
994 content.push_str(&crate::template_env::render(
995 "php_phpdoc_array_param.jinja",
996 context! {
997 nullable_prefix => nullable_prefix,
998 phpdoc => &phpdoc,
999 param_name => &f.name,
1000 },
1001 ));
1002 }
1003 content.push_str(" */\n");
1004 }
1005
1006 let params: Vec<String> = sorted_fields
1007 .iter()
1008 .map(|f| {
1009 let ptype = php_type(&f.ty);
1010 let nullable = if f.optional && !ptype.starts_with('?') {
1011 format!("?{ptype}")
1012 } else {
1013 ptype
1014 };
1015 let default = if f.optional { " = null" } else { "" };
1016 format!(" {} ${}{}", nullable, f.name, default)
1017 })
1018 .collect();
1019 content.push_str(&crate::template_env::render(
1020 "php_constructor_method.jinja",
1021 context! { params => ¶ms.join(",\n") },
1022 ));
1023
1024 for field in &typ.fields {
1026 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
1027 let return_type = if field.optional {
1028 let inner = php_type(&field.ty);
1029 if inner.starts_with('?') {
1030 inner
1031 } else {
1032 format!("?{inner}")
1033 }
1034 } else {
1035 php_type(&field.ty)
1036 };
1037 let getter_name = field.name.to_lower_camel_case();
1038 if is_array {
1040 let phpdoc = php_phpdoc_type(&field.ty);
1041 let nullable_prefix = if field.optional { "?" } else { "" };
1042 content.push_str(&crate::template_env::render(
1043 "php_constructor_doc_return.jinja",
1044 context! { return_type => &format!("{nullable_prefix}{phpdoc}") },
1045 ));
1046 }
1047 let is_void_getter = return_type == "void";
1048 let getter_body = if is_void_getter {
1049 "{ }".to_string()
1050 } else {
1051 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1052 };
1053 content.push_str(&crate::template_env::render(
1054 "php_getter_stub.jinja",
1055 context! {
1056 getter_name => &format!("get{}", getter_name.to_pascal_case()),
1057 return_type => &return_type,
1058 getter_body => &getter_body,
1059 },
1060 ));
1061 }
1062
1063 content.push_str("}\n\n");
1064 }
1065
1066 for enum_def in &api.enums {
1069 if is_tagged_data_enum(enum_def) {
1070 if !enum_def.doc.is_empty() {
1072 content.push_str("/**\n");
1073 content.push_str(&crate::template_env::render(
1074 "php_phpdoc_lines.jinja",
1075 context! {
1076 doc_lines => enum_def.doc.lines().collect::<Vec<_>>(),
1077 indent => "",
1078 },
1079 ));
1080 content.push_str(" */\n");
1081 }
1082 content.push_str(&crate::template_env::render(
1083 "php_record_class_stub_declaration.jinja",
1084 context! { class_name => &enum_def.name },
1085 ));
1086 content.push_str("}\n\n");
1087 } else {
1088 content.push_str(&crate::template_env::render(
1090 "php_tagged_enum_declaration.jinja",
1091 context! { enum_name => &enum_def.name },
1092 ));
1093 for variant in &enum_def.variants {
1094 let case_name = sanitize_php_enum_case(&variant.name);
1095 content.push_str(&crate::template_env::render(
1096 "php_enum_variant_stub.jinja",
1097 context! {
1098 variant_name => case_name,
1099 value => &variant.name,
1100 },
1101 ));
1102 }
1103 content.push_str("}\n\n");
1104 }
1105 }
1106
1107 if !api.functions.is_empty() {
1112 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1114 .trait_bridges
1115 .iter()
1116 .filter_map(|b| b.param_name.as_deref())
1117 .collect();
1118
1119 content.push_str(&crate::template_env::render(
1120 "php_api_class_declaration.jinja",
1121 context! { class_name => &class_name },
1122 ));
1123 for func in &api.functions {
1124 let return_type = php_type_fq(&func.return_type, &namespace);
1125 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1126 let visible_params: Vec<_> = func
1128 .params
1129 .iter()
1130 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1131 .collect();
1132 let has_array_params = visible_params
1139 .iter()
1140 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1141 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1142 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1143 if has_array_params || has_array_return {
1144 content.push_str(" /**\n");
1145 for p in &visible_params {
1146 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1147 let nullable_prefix = if p.optional { "?" } else { "" };
1148 content.push_str(&crate::template_env::render(
1149 "php_phpdoc_static_param.jinja",
1150 context! {
1151 nullable_prefix => nullable_prefix,
1152 ptype => &ptype,
1153 param_name => &p.name,
1154 },
1155 ));
1156 }
1157 content.push_str(&crate::template_env::render(
1158 "php_phpdoc_static_return.jinja",
1159 context! { return_phpdoc => &return_phpdoc },
1160 ));
1161 content.push_str(" */\n");
1162 }
1163 let params: Vec<String> = visible_params
1164 .iter()
1165 .map(|p| {
1166 let ptype = php_type_fq(&p.ty, &namespace);
1167 if p.optional {
1168 format!("?{} ${} = null", ptype, p.name)
1169 } else {
1170 format!("{} ${}", ptype, p.name)
1171 }
1172 })
1173 .collect();
1174 let stub_method_name = if func.is_async {
1176 format!("{}_async", func.name).to_lower_camel_case()
1177 } else {
1178 func.name.to_lower_camel_case()
1179 };
1180 let is_void_stub = return_type == "void";
1181 let stub_body = if is_void_stub {
1182 "{ }".to_string()
1183 } else {
1184 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1185 };
1186 content.push_str(&crate::template_env::render(
1187 "php_static_method_stub.jinja",
1188 context! {
1189 method_name => &stub_method_name,
1190 params => ¶ms.join(", "),
1191 return_type => &return_type,
1192 stub_body => &stub_body,
1193 },
1194 ));
1195 }
1196 content.push_str("}\n\n");
1197 }
1198
1199 content.push_str(&crate::template_env::render(
1201 "php_namespace_block_end.jinja",
1202 minijinja::Value::default(),
1203 ));
1204
1205 let output_dir = config
1207 .php
1208 .as_ref()
1209 .and_then(|p| p.stubs.as_ref())
1210 .map(|s| s.output.to_string_lossy().to_string())
1211 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1212
1213 Ok(vec![GeneratedFile {
1214 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1215 content,
1216 generated_header: false,
1217 }])
1218 }
1219
1220 fn build_config(&self) -> Option<BuildConfig> {
1221 Some(BuildConfig {
1222 tool: "cargo",
1223 crate_suffix: "-php",
1224 build_dep: BuildDependency::None,
1225 post_build: vec![],
1226 })
1227 }
1228}
1229
1230fn php_phpdoc_type(ty: &TypeRef) -> String {
1233 match ty {
1234 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1235 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1236 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1237 _ => php_type(ty),
1238 }
1239}
1240
1241fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1243 match ty {
1244 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1245 TypeRef::Map(k, v) => format!(
1246 "array<{}, {}>",
1247 php_phpdoc_type_fq(k, namespace),
1248 php_phpdoc_type_fq(v, namespace)
1249 ),
1250 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1251 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1252 _ => php_type(ty),
1253 }
1254}
1255
1256fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1258 match ty {
1259 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1260 TypeRef::Optional(inner) => {
1261 let inner_type = php_type_fq(inner, namespace);
1262 if inner_type.starts_with('?') {
1263 inner_type
1264 } else {
1265 format!("?{inner_type}")
1266 }
1267 }
1268 _ => php_type(ty),
1269 }
1270}
1271
1272fn php_type(ty: &TypeRef) -> String {
1274 match ty {
1275 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1276 TypeRef::Primitive(p) => match p {
1277 PrimitiveType::Bool => "bool".to_string(),
1278 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1279 PrimitiveType::U8
1280 | PrimitiveType::U16
1281 | PrimitiveType::U32
1282 | PrimitiveType::U64
1283 | PrimitiveType::I8
1284 | PrimitiveType::I16
1285 | PrimitiveType::I32
1286 | PrimitiveType::I64
1287 | PrimitiveType::Usize
1288 | PrimitiveType::Isize => "int".to_string(),
1289 },
1290 TypeRef::Optional(inner) => {
1291 let inner_type = php_type(inner);
1294 if inner_type.starts_with('?') {
1295 inner_type
1296 } else {
1297 format!("?{inner_type}")
1298 }
1299 }
1300 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1301 TypeRef::Named(name) => name.clone(),
1302 TypeRef::Unit => "void".to_string(),
1303 TypeRef::Duration => "float".to_string(),
1304 }
1305}