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