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 for line in func.doc.lines() {
677 if line.is_empty() {
678 content.push_str(&crate::template_env::render(
679 "php_phpdoc_empty_line.jinja",
680 minijinja::Value::default(),
681 ));
682 } else {
683 content.push_str(&crate::template_env::render(
684 "php_phpdoc_text_line.jinja",
685 context! { text => line },
686 ));
687 }
688 }
689 if func.doc.is_empty() {
690 content.push_str(&crate::template_env::render(
691 "php_phpdoc_text_line.jinja",
692 context! { text => &format!("{}.", method_name) },
693 ));
694 }
695 content.push_str(&crate::template_env::render(
696 "php_phpdoc_empty_line.jinja",
697 minijinja::Value::default(),
698 ));
699 for p in &visible_params {
700 let ptype = php_phpdoc_type(&p.ty);
701 let nullable_prefix = if p.optional { "?" } else { "" };
702 content.push_str(&crate::template_env::render(
703 "php_phpdoc_param_line.jinja",
704 context! {
705 nullable_prefix => nullable_prefix,
706 param_type => &ptype,
707 param_name => &p.name,
708 },
709 ));
710 }
711 let return_phpdoc = php_phpdoc_type(&func.return_type);
712 content.push_str(&crate::template_env::render(
713 "php_phpdoc_return_line.jinja",
714 context! { return_type => &return_phpdoc },
715 ));
716 if func.error_type.is_some() {
717 content.push_str(&crate::template_env::render(
718 "php_phpdoc_throws_line.jinja",
719 context! {
720 namespace => namespace.as_str(),
721 class_name => &class_name,
722 },
723 ));
724 }
725 content.push_str(&crate::template_env::render(
726 "php_phpdoc_block_end.jinja",
727 minijinja::Value::default(),
728 ));
729
730 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
741 if let TypeRef::Named(name) = &p.ty {
742 (name.ends_with("Config") || name.as_str() == "config")
743 && no_arg_constructor_types.contains(name.as_str())
744 } else {
745 false
746 }
747 };
748
749 let mut first_optional_idx = None;
750 for (idx, p) in visible_params.iter().enumerate() {
751 if p.optional || is_optional_config_param(p) {
752 first_optional_idx = Some(idx);
753 break;
754 }
755 }
756
757 content.push_str(&crate::template_env::render(
758 "php_method_signature_start.jinja",
759 context! { method_name => &method_name },
760 ));
761
762 let params: Vec<String> = visible_params
763 .iter()
764 .enumerate()
765 .map(|(idx, p)| {
766 let ptype = php_type(&p.ty);
767 let should_be_optional = p.optional
772 || is_optional_config_param(p)
773 || first_optional_idx.is_some_and(|first| idx >= first);
774 if should_be_optional {
775 format!("?{} ${} = null", ptype, p.name)
776 } else {
777 format!("{} ${}", ptype, p.name)
778 }
779 })
780 .collect();
781 content.push_str(¶ms.join(", "));
782 content.push_str(&crate::template_env::render(
783 "php_method_signature_end.jinja",
784 context! { return_type => &return_php_type },
785 ));
786 let ext_method_name = if func.is_async {
791 format!("{}_async", func.name).to_lower_camel_case()
792 } else {
793 func.name.to_lower_camel_case()
794 };
795 let is_void = matches!(&func.return_type, TypeRef::Unit);
796 let call_params = visible_params
804 .iter()
805 .enumerate()
806 .map(|(idx, p)| {
807 let should_be_optional = p.optional
808 || is_optional_config_param(p)
809 || first_optional_idx.is_some_and(|first| idx >= first);
810 if should_be_optional && is_optional_config_param(p) {
811 if let TypeRef::Named(type_name) = &p.ty {
812 return format!("${} ?? new {}()", p.name, type_name);
813 }
814 }
815 format!("${}", p.name)
816 })
817 .collect::<Vec<_>>()
818 .join(", ");
819 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
820 if is_void {
821 content.push_str(&crate::template_env::render(
822 "php_method_call_statement.jinja",
823 context! { call_expr => &call_expr },
824 ));
825 } else {
826 content.push_str(&crate::template_env::render(
827 "php_method_call_return.jinja",
828 context! { call_expr => &call_expr },
829 ));
830 }
831 content.push_str(&crate::template_env::render(
832 "php_method_end.jinja",
833 minijinja::Value::default(),
834 ));
835 }
836
837 content.push_str(&crate::template_env::render(
838 "php_class_end.jinja",
839 minijinja::Value::default(),
840 ));
841
842 let output_dir = config
846 .php
847 .as_ref()
848 .and_then(|p| p.stubs.as_ref())
849 .map(|s| s.output.to_string_lossy().to_string())
850 .unwrap_or_else(|| "packages/php/src/".to_string());
851
852 Ok(vec![GeneratedFile {
853 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
854 content,
855 generated_header: false,
856 }])
857 }
858
859 fn generate_type_stubs(
860 &self,
861 api: &ApiSurface,
862 config: &ResolvedCrateConfig,
863 ) -> anyhow::Result<Vec<GeneratedFile>> {
864 let extension_name = config.php_extension_name();
865 let class_name = extension_name.to_pascal_case();
866
867 let namespace = php_autoload_namespace(config);
869
870 let mut content = String::new();
875 content.push_str(&crate::template_env::render(
876 "php_file_header.jinja",
877 minijinja::Value::default(),
878 ));
879 content.push_str(&hash::header(CommentStyle::DoubleSlash));
880 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
881 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
882 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
883 content.push_str(&crate::template_env::render(
884 "php_declare_strict_types.jinja",
885 minijinja::Value::default(),
886 ));
887 content.push_str(&crate::template_env::render(
889 "php_namespace_block_begin.jinja",
890 context! { namespace => &namespace },
891 ));
892
893 content.push_str(&crate::template_env::render(
895 "php_exception_class_declaration.jinja",
896 context! { class_name => &class_name },
897 ));
898 content.push_str(
899 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
900 );
901 content.push_str("}\n\n");
902
903 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
905 if typ.is_opaque {
906 if !typ.doc.is_empty() {
907 content.push_str("/**\n");
908 for line in typ.doc.lines() {
909 if line.is_empty() {
910 content.push_str(" *\n");
911 } else {
912 content.push_str(&crate::template_env::render(
913 "php_phpdoc_doc_line.jinja",
914 context! { line => line },
915 ));
916 }
917 }
918 content.push_str(" */\n");
919 }
920 content.push_str(&crate::template_env::render(
921 "php_opaque_class_stub_declaration.jinja",
922 context! { class_name => &typ.name },
923 ));
924 content.push_str("}\n\n");
926 }
927 }
928
929 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
931 if typ.is_opaque || typ.fields.is_empty() {
932 continue;
933 }
934 if !typ.doc.is_empty() {
935 content.push_str("/**\n");
936 for line in typ.doc.lines() {
937 if line.is_empty() {
938 content.push_str(" *\n");
939 } else {
940 content.push_str(&crate::template_env::render(
941 "php_phpdoc_doc_line.jinja",
942 context! { line => line },
943 ));
944 }
945 }
946 content.push_str(" */\n");
947 }
948 content.push_str(&crate::template_env::render(
949 "php_record_class_stub_declaration.jinja",
950 context! { class_name => &typ.name },
951 ));
952
953 for field in &typ.fields {
955 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
956 let prop_type = if field.optional {
957 let inner = php_type(&field.ty);
958 if inner.starts_with('?') {
959 inner
960 } else {
961 format!("?{inner}")
962 }
963 } else {
964 php_type(&field.ty)
965 };
966 if is_array {
967 let phpdoc = php_phpdoc_type(&field.ty);
968 let nullable_prefix = if field.optional { "?" } else { "" };
969 content.push_str(&crate::template_env::render(
970 "php_property_type_annotation.jinja",
971 context! {
972 nullable_prefix => nullable_prefix,
973 phpdoc => &phpdoc,
974 },
975 ));
976 }
977 content.push_str(&crate::template_env::render(
978 "php_property_stub.jinja",
979 context! {
980 prop_type => &prop_type,
981 field_name => &field.name,
982 },
983 ));
984 }
985 content.push('\n');
986
987 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
991 sorted_fields.sort_by_key(|f| f.optional);
992
993 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
996 .iter()
997 .copied()
998 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
999 .collect();
1000 if !array_fields.is_empty() {
1001 content.push_str(" /**\n");
1002 for f in &array_fields {
1003 let phpdoc = php_phpdoc_type(&f.ty);
1004 let nullable_prefix = if f.optional { "?" } else { "" };
1005 content.push_str(&crate::template_env::render(
1006 "php_phpdoc_array_param.jinja",
1007 context! {
1008 nullable_prefix => nullable_prefix,
1009 phpdoc => &phpdoc,
1010 param_name => &f.name,
1011 },
1012 ));
1013 }
1014 content.push_str(" */\n");
1015 }
1016
1017 let params: Vec<String> = sorted_fields
1018 .iter()
1019 .map(|f| {
1020 let ptype = php_type(&f.ty);
1021 let nullable = if f.optional && !ptype.starts_with('?') {
1022 format!("?{ptype}")
1023 } else {
1024 ptype
1025 };
1026 let default = if f.optional { " = null" } else { "" };
1027 format!(" {} ${}{}", nullable, f.name, default)
1028 })
1029 .collect();
1030 content.push_str(&crate::template_env::render(
1031 "php_constructor_method.jinja",
1032 context! { params => ¶ms.join(",\n") },
1033 ));
1034
1035 for field in &typ.fields {
1037 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
1038 let return_type = if field.optional {
1039 let inner = php_type(&field.ty);
1040 if inner.starts_with('?') {
1041 inner
1042 } else {
1043 format!("?{inner}")
1044 }
1045 } else {
1046 php_type(&field.ty)
1047 };
1048 let getter_name = field.name.to_lower_camel_case();
1049 if is_array {
1051 let phpdoc = php_phpdoc_type(&field.ty);
1052 let nullable_prefix = if field.optional { "?" } else { "" };
1053 content.push_str(&crate::template_env::render(
1054 "php_constructor_doc_return.jinja",
1055 context! { return_type => &format!("{nullable_prefix}{phpdoc}") },
1056 ));
1057 }
1058 let is_void_getter = return_type == "void";
1059 let getter_body = if is_void_getter {
1060 "{ }".to_string()
1061 } else {
1062 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1063 };
1064 content.push_str(&crate::template_env::render(
1065 "php_getter_stub.jinja",
1066 context! {
1067 getter_name => &format!("get{}", getter_name.to_pascal_case()),
1068 return_type => &return_type,
1069 getter_body => &getter_body,
1070 },
1071 ));
1072 }
1073
1074 content.push_str("}\n\n");
1075 }
1076
1077 for enum_def in &api.enums {
1080 if is_tagged_data_enum(enum_def) {
1081 if !enum_def.doc.is_empty() {
1083 content.push_str("/**\n");
1084 for line in enum_def.doc.lines() {
1085 if line.is_empty() {
1086 content.push_str(" *\n");
1087 } else {
1088 content.push_str(&crate::template_env::render(
1089 "php_phpdoc_doc_line.jinja",
1090 context! { line => line },
1091 ));
1092 }
1093 }
1094 content.push_str(" */\n");
1095 }
1096 content.push_str(&crate::template_env::render(
1097 "php_record_class_stub_declaration.jinja",
1098 context! { class_name => &enum_def.name },
1099 ));
1100 content.push_str("}\n\n");
1101 } else {
1102 content.push_str(&crate::template_env::render(
1104 "php_tagged_enum_declaration.jinja",
1105 context! { enum_name => &enum_def.name },
1106 ));
1107 for variant in &enum_def.variants {
1108 let case_name = sanitize_php_enum_case(&variant.name);
1109 content.push_str(&crate::template_env::render(
1110 "php_enum_variant_stub.jinja",
1111 context! {
1112 variant_name => case_name,
1113 value => &variant.name,
1114 },
1115 ));
1116 }
1117 content.push_str("}\n\n");
1118 }
1119 }
1120
1121 if !api.functions.is_empty() {
1126 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1128 .trait_bridges
1129 .iter()
1130 .filter_map(|b| b.param_name.as_deref())
1131 .collect();
1132
1133 content.push_str(&crate::template_env::render(
1134 "php_api_class_declaration.jinja",
1135 context! { class_name => &class_name },
1136 ));
1137 for func in &api.functions {
1138 let return_type = php_type_fq(&func.return_type, &namespace);
1139 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1140 let visible_params: Vec<_> = func
1142 .params
1143 .iter()
1144 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1145 .collect();
1146 let has_array_params = visible_params
1153 .iter()
1154 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1155 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1156 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1157 if has_array_params || has_array_return {
1158 content.push_str(" /**\n");
1159 for p in &visible_params {
1160 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1161 let nullable_prefix = if p.optional { "?" } else { "" };
1162 content.push_str(&crate::template_env::render(
1163 "php_phpdoc_static_param.jinja",
1164 context! {
1165 nullable_prefix => nullable_prefix,
1166 ptype => &ptype,
1167 param_name => &p.name,
1168 },
1169 ));
1170 }
1171 content.push_str(&crate::template_env::render(
1172 "php_phpdoc_static_return.jinja",
1173 context! { return_phpdoc => &return_phpdoc },
1174 ));
1175 content.push_str(" */\n");
1176 }
1177 let params: Vec<String> = visible_params
1178 .iter()
1179 .map(|p| {
1180 let ptype = php_type_fq(&p.ty, &namespace);
1181 if p.optional {
1182 format!("?{} ${} = null", ptype, p.name)
1183 } else {
1184 format!("{} ${}", ptype, p.name)
1185 }
1186 })
1187 .collect();
1188 let stub_method_name = if func.is_async {
1190 format!("{}_async", func.name).to_lower_camel_case()
1191 } else {
1192 func.name.to_lower_camel_case()
1193 };
1194 let is_void_stub = return_type == "void";
1195 let stub_body = if is_void_stub {
1196 "{ }".to_string()
1197 } else {
1198 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1199 };
1200 content.push_str(&crate::template_env::render(
1201 "php_static_method_stub.jinja",
1202 context! {
1203 method_name => &stub_method_name,
1204 params => ¶ms.join(", "),
1205 return_type => &return_type,
1206 stub_body => &stub_body,
1207 },
1208 ));
1209 }
1210 content.push_str("}\n\n");
1211 }
1212
1213 content.push_str(&crate::template_env::render(
1215 "php_namespace_block_end.jinja",
1216 minijinja::Value::default(),
1217 ));
1218
1219 let output_dir = config
1221 .php
1222 .as_ref()
1223 .and_then(|p| p.stubs.as_ref())
1224 .map(|s| s.output.to_string_lossy().to_string())
1225 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1226
1227 Ok(vec![GeneratedFile {
1228 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1229 content,
1230 generated_header: false,
1231 }])
1232 }
1233
1234 fn build_config(&self) -> Option<BuildConfig> {
1235 Some(BuildConfig {
1236 tool: "cargo",
1237 crate_suffix: "-php",
1238 build_dep: BuildDependency::None,
1239 post_build: vec![],
1240 })
1241 }
1242}
1243
1244fn php_phpdoc_type(ty: &TypeRef) -> String {
1247 match ty {
1248 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1249 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1250 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1251 _ => php_type(ty),
1252 }
1253}
1254
1255fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1257 match ty {
1258 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1259 TypeRef::Map(k, v) => format!(
1260 "array<{}, {}>",
1261 php_phpdoc_type_fq(k, namespace),
1262 php_phpdoc_type_fq(v, namespace)
1263 ),
1264 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1265 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1266 _ => php_type(ty),
1267 }
1268}
1269
1270fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1272 match ty {
1273 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1274 TypeRef::Optional(inner) => {
1275 let inner_type = php_type_fq(inner, namespace);
1276 if inner_type.starts_with('?') {
1277 inner_type
1278 } else {
1279 format!("?{inner_type}")
1280 }
1281 }
1282 _ => php_type(ty),
1283 }
1284}
1285
1286fn php_type(ty: &TypeRef) -> String {
1288 match ty {
1289 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1290 TypeRef::Primitive(p) => match p {
1291 PrimitiveType::Bool => "bool".to_string(),
1292 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1293 PrimitiveType::U8
1294 | PrimitiveType::U16
1295 | PrimitiveType::U32
1296 | PrimitiveType::U64
1297 | PrimitiveType::I8
1298 | PrimitiveType::I16
1299 | PrimitiveType::I32
1300 | PrimitiveType::I64
1301 | PrimitiveType::Usize
1302 | PrimitiveType::Isize => "int".to_string(),
1303 },
1304 TypeRef::Optional(inner) => {
1305 let inner_type = php_type(inner);
1308 if inner_type.starts_with('?') {
1309 inner_type
1310 } else {
1311 format!("?{inner_type}")
1312 }
1313 }
1314 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1315 TypeRef::Named(name) => name.clone(),
1316 TypeRef::Unit => "void".to_string(),
1317 TypeRef::Duration => "float".to_string(),
1318 }
1319}