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 never_skip_cfg_field_names: &[],
68 }
69 }
70}
71
72impl Backend for PhpBackend {
73 fn name(&self) -> &str {
74 "php"
75 }
76
77 fn language(&self) -> Language {
78 Language::Php
79 }
80
81 fn capabilities(&self) -> Capabilities {
82 Capabilities {
83 supports_async: false,
84 supports_classes: true,
85 supports_enums: true,
86 supports_option: true,
87 supports_result: true,
88 ..Capabilities::default()
89 }
90 }
91
92 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
93 let data_enum_names: AHashSet<String> = api
96 .enums
97 .iter()
98 .filter(|e| is_tagged_data_enum(e))
99 .map(|e| e.name.clone())
100 .collect();
101 let untagged_data_enum_names: AHashSet<String> = api
102 .enums
103 .iter()
104 .filter(|e| is_untagged_data_enum(e))
105 .map(|e| e.name.clone())
106 .collect();
107 let enum_names: AHashSet<String> = api
110 .enums
111 .iter()
112 .filter(|e| !is_tagged_data_enum(e) && !is_untagged_data_enum(e))
113 .map(|e| e.name.clone())
114 .collect();
115 let mapper = PhpMapper {
116 enum_names: enum_names.clone(),
117 data_enum_names: data_enum_names.clone(),
118 untagged_data_enum_names: untagged_data_enum_names.clone(),
119 };
120 let core_import = config.core_import_name();
121 let lang_rename_all = config.serde_rename_all_for_language(Language::Php);
122
123 let php_config = config.php.as_ref();
125 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
126 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
127
128 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
129 let has_serde = detect_serde_available(&output_dir);
130
131 let bridge_type_aliases_php: Vec<String> = config
137 .trait_bridges
138 .iter()
139 .filter_map(|b| b.type_alias.clone())
140 .collect();
141 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
142 let mut opaque_names_vec_php: Vec<String> = api
143 .types
144 .iter()
145 .filter(|t| t.is_opaque)
146 .map(|t| t.name.clone())
147 .collect();
148 opaque_names_vec_php.extend(bridge_type_aliases_php);
149
150 let mut cfg = Self::binding_config(&core_import, has_serde);
151 cfg.opaque_type_names = &opaque_names_vec_php;
152 let never_skip_cfg_field_names: Vec<String> = config
153 .trait_bridges
154 .iter()
155 .filter_map(|b| {
156 if b.bind_via == alef_core::config::BridgeBinding::OptionsField {
157 b.resolved_options_field().map(|s| s.to_string())
158 } else {
159 None
160 }
161 })
162 .collect();
163 cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;
164
165 let mut builder = RustFileBuilder::new().with_generated_header();
167 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
168 builder.add_inner_attribute("allow(unsafe_code)");
169 builder.add_inner_attribute("allow(non_snake_case)");
171 builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions, clippy::arc_with_non_send_sync, clippy::collapsible_if, clippy::clone_on_copy, clippy::should_implement_trait)");
172 builder.add_import("ext_php_rs::prelude::*");
173
174 if has_serde {
176 builder.add_import("serde_json");
177 }
178
179 for trait_path in generators::collect_trait_imports(api) {
181 builder.add_import(&trait_path);
182 }
183
184 let has_maps = api.types.iter().any(|t| {
186 t.fields
187 .iter()
188 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
189 }) || api
190 .functions
191 .iter()
192 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
193 if has_maps {
194 builder.add_import("std::collections::HashMap");
195 }
196
197 builder.add_item(
202 "#[derive(Debug, Clone, Default)]\n\
203 pub struct PhpBytes(pub Vec<u8>);\n\
204 \n\
205 impl<'a> ext_php_rs::convert::FromZval<'a> for PhpBytes {\n \
206 const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::String;\n \
207 fn from_zval(zval: &'a ext_php_rs::types::Zval) -> Option<Self> {\n \
208 zval.zend_str().map(|zs| PhpBytes(zs.as_bytes().to_vec()))\n \
209 }\n\
210 }\n\
211 \n\
212 impl From<PhpBytes> for Vec<u8> {\n \
213 fn from(b: PhpBytes) -> Self { b.0 }\n\
214 }\n\
215 \n\
216 impl From<Vec<u8>> for PhpBytes {\n \
217 fn from(v: Vec<u8>) -> Self { PhpBytes(v) }\n\
218 }\n",
219 );
220
221 let custom_mods = config.custom_modules.for_language(Language::Php);
223 for module in custom_mods {
224 builder.add_item(&format!("pub mod {module};"));
225 }
226
227 let has_async =
229 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
230
231 if has_async {
232 builder.add_item(&gen_tokio_runtime());
233 }
234
235 let opaque_types: AHashSet<String> = api
237 .types
238 .iter()
239 .filter(|t| t.is_opaque)
240 .map(|t| t.name.clone())
241 .collect();
242 if !opaque_types.is_empty() {
243 builder.add_import("std::sync::Arc");
244 }
245
246 let mutex_types: AHashSet<String> = api
248 .types
249 .iter()
250 .filter(|t| t.is_opaque && alef_codegen::generators::type_needs_mutex(t))
251 .map(|t| t.name.clone())
252 .collect();
253 if !mutex_types.is_empty() {
254 builder.add_import("std::sync::Mutex");
255 }
256
257 let extension_name = config.php_extension_name();
260 let php_namespace = php_autoload_namespace(config);
261
262 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
264
265 for adapter in &config.adapters {
267 match adapter.pattern {
268 alef_core::config::AdapterPattern::Streaming => {
269 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
270 if let Some(struct_code) = adapter_bodies.get(&key) {
271 builder.add_item(struct_code);
272 }
273 }
274 alef_core::config::AdapterPattern::CallbackBridge => {
275 let struct_key = format!("{}.__bridge_struct__", adapter.name);
276 let impl_key = format!("{}.__bridge_impl__", adapter.name);
277 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
278 builder.add_item(struct_code);
279 }
280 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
281 builder.add_item(impl_code);
282 }
283 }
284 _ => {}
285 }
286 }
287
288 for typ in api
289 .types
290 .iter()
291 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
292 {
293 if typ.is_opaque {
294 let ns_escaped = php_namespace.replace('\\', "\\\\");
298 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
299 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
300 let opaque_cfg = RustBindingConfig {
301 struct_attrs: &opaque_attr_arr,
302 ..cfg
303 };
304 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
305 builder.add_item(&gen_opaque_struct_methods(
306 typ,
307 &mapper,
308 &opaque_types,
309 &core_import,
310 &adapter_bodies,
311 &mutex_types,
312 ));
313 } else {
314 builder.add_item(&gen_php_struct(
317 typ,
318 &mapper,
319 &cfg,
320 Some(&php_namespace),
321 &enum_names,
322 &lang_rename_all,
323 ));
324 builder.add_item(&types::gen_struct_methods_with_exclude(
325 typ,
326 &mapper,
327 has_serde,
328 &core_import,
329 &opaque_types,
330 &enum_names,
331 &api.enums,
332 &exclude_functions,
333 &bridge_type_aliases_set,
334 &never_skip_cfg_field_names,
335 &mutex_types,
336 ));
337 }
338 }
339
340 for enum_def in &api.enums {
341 if is_tagged_data_enum(enum_def) {
342 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
344 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
345 } else {
346 builder.add_item(&gen_enum_constants(enum_def));
347 }
348 }
349
350 let included_functions: Vec<_> = api
355 .functions
356 .iter()
357 .filter(|f| !exclude_functions.contains(&f.name))
358 .collect();
359 if !included_functions.is_empty() {
360 let facade_class_name = extension_name.to_pascal_case();
361 let mut method_items: Vec<String> = Vec::new();
364 for func in included_functions {
365 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
366 if let Some((param_idx, bridge_cfg)) = bridge_param {
367 method_items.push(crate::trait_bridge::gen_bridge_function(
368 func,
369 param_idx,
370 bridge_cfg,
371 &mapper,
372 &opaque_types,
373 &core_import,
374 ));
375 } else if func.is_async {
376 method_items.push(gen_async_function_as_static_method(
377 func,
378 &mapper,
379 &opaque_types,
380 &core_import,
381 &config.trait_bridges,
382 &mutex_types,
383 ));
384 } else {
385 method_items.push(gen_function_as_static_method(
386 func,
387 &mapper,
388 &opaque_types,
389 &core_import,
390 &config.trait_bridges,
391 has_serde,
392 &mutex_types,
393 ));
394 }
395 }
396
397 let methods_joined = method_items
398 .iter()
399 .map(|m| {
400 m.lines()
402 .map(|l| {
403 if l.is_empty() {
404 String::new()
405 } else {
406 format!(" {l}")
407 }
408 })
409 .collect::<Vec<_>>()
410 .join("\n")
411 })
412 .collect::<Vec<_>>()
413 .join("\n\n");
414 let php_api_class_name = format!("{facade_class_name}Api");
417 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
419 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
420 let facade_struct = format!(
421 "#[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}}"
422 );
423 builder.add_item(&facade_struct);
424
425 for bridge_cfg in &config.trait_bridges {
427 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
428 let bridge = crate::trait_bridge::gen_trait_bridge(
429 trait_type,
430 bridge_cfg,
431 &core_import,
432 &config.error_type_name(),
433 &config.error_constructor_expr(),
434 api,
435 );
436 for imp in &bridge.imports {
437 builder.add_import(imp);
438 }
439 builder.add_item(&bridge.code);
440 }
441 }
442 }
443
444 let convertible = alef_codegen::conversions::convertible_types(api);
445 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
446 let input_types = alef_codegen::conversions::input_type_names(api);
447 let enum_names_ref = &mapper.enum_names;
452 let bridge_skip_types: Vec<String> = config
453 .trait_bridges
454 .iter()
455 .filter(|b| !matches!(b.bind_via, alef_core::config::BridgeBinding::OptionsField))
456 .filter_map(|b| b.type_alias.clone())
457 .collect();
458 let trait_bridge_arc_wrapper_field_names: Vec<String> = config
463 .trait_bridges
464 .iter()
465 .filter(|b| b.bind_via == alef_core::config::BridgeBinding::OptionsField)
466 .filter_map(|b| b.resolved_options_field().map(String::from))
467 .collect();
468 let mut conv_opaque_types: AHashSet<String> = opaque_types.clone();
473 for bridge in &config.trait_bridges {
474 if let Some(alias) = &bridge.type_alias {
475 conv_opaque_types.insert(alias.clone());
476 }
477 }
478 let php_conv_config = ConversionConfig {
479 cast_large_ints_to_i64: true,
480 enum_string_names: Some(enum_names_ref),
481 untagged_data_enum_names: Some(&mapper.untagged_data_enum_names),
482 json_as_value: true,
486 include_cfg_metadata: false,
487 option_duration_on_defaults: true,
488 from_binding_skip_types: &bridge_skip_types,
489 never_skip_cfg_field_names: &never_skip_cfg_field_names,
490 opaque_types: Some(&conv_opaque_types),
491 trait_bridge_arc_wrapper_field_names: &trait_bridge_arc_wrapper_field_names,
492 ..Default::default()
493 };
494 let mut enum_tainted: AHashSet<String> = AHashSet::new();
496 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
497 if has_enum_named_field(typ, enum_names_ref) {
498 enum_tainted.insert(typ.name.clone());
499 }
500 }
501 let mut changed = true;
503 while changed {
504 changed = false;
505 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
506 if !enum_tainted.contains(&typ.name)
507 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
508 {
509 enum_tainted.insert(typ.name.clone());
510 changed = true;
511 }
512 }
513 }
514 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
515 if input_types.contains(&typ.name)
517 && !enum_tainted.contains(&typ.name)
518 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
519 {
520 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
521 typ,
522 &core_import,
523 &php_conv_config,
524 ));
525 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
526 builder.add_item(&gen_enum_tainted_from_binding_to_core(
533 typ,
534 &core_import,
535 enum_names_ref,
536 &enum_tainted,
537 &php_conv_config,
538 &api.enums,
539 &bridge_type_aliases_set,
540 ));
541 }
542 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
544 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
545 typ,
546 &core_import,
547 &opaque_types,
548 &php_conv_config,
549 ));
550 }
551 }
552
553 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
555 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
556 }
557
558 for error in &api.errors {
560 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
561 }
562
563 if has_serde {
567 let serde_module = "mod serde_defaults {\n pub fn bool_true() -> bool { true }\n\
568 pub fn max_archive_size() -> i64 { 500 * 1024 * 1024 }\n\
569 pub fn max_compression_ratio() -> i64 { 100 }\n\
570 pub fn max_files_in_archive() -> i64 { 10_000 }\n\
571 pub fn max_nesting_depth() -> i64 { 1024 }\n\
572 pub fn max_entity_length() -> i64 { 1024 * 1024 }\n\
573 pub fn max_content_size() -> i64 { 100 * 1024 * 1024 }\n\
574 pub fn max_iterations() -> i64 { 10_000_000 }\n\
575 pub fn max_xml_depth() -> i64 { 1024 }\n\
576 pub fn max_table_cells() -> i64 { 100_000 }\n\
577 }";
578 builder.add_item(serde_module);
579 }
580
581 let php_config = config.php.as_ref();
587 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
588
589 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
593 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
594 }
595
596 let mut class_registrations = String::new();
599 for typ in api
600 .types
601 .iter()
602 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
603 {
604 class_registrations.push_str(&crate::template_env::render(
605 "php_class_registration.jinja",
606 context! { class_name => &typ.name },
607 ));
608 }
609 if !api.functions.is_empty() {
611 let facade_class_name = extension_name.to_pascal_case();
612 class_registrations.push_str(&crate::template_env::render(
613 "php_class_registration.jinja",
614 context! { class_name => &format!("{facade_class_name}Api") },
615 ));
616 }
617 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
620 class_registrations.push_str(&crate::template_env::render(
621 "php_class_registration.jinja",
622 context! { class_name => &enum_def.name },
623 ));
624 }
625 builder.add_item(&format!(
626 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
627 ));
628
629 let mut content = builder.build();
630
631 for bridge in &config.trait_bridges {
636 if let Some(field_name) = bridge.resolved_options_field() {
637 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
638 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
639 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
640 let builder_type = format!("{}Builder", options_type);
641 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
642
643 let old_method = format!(
649 " 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 }}"
650 );
651 let new_method = format!(
652 " 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 }}"
653 );
654
655 content = content.replace(&old_method, &new_method);
656 }
657 }
658
659 Ok(vec![GeneratedFile {
660 path: PathBuf::from(&output_dir).join("lib.rs"),
661 content,
662 generated_header: false,
663 }])
664 }
665
666 fn generate_public_api(
667 &self,
668 api: &ApiSurface,
669 config: &ResolvedCrateConfig,
670 ) -> anyhow::Result<Vec<GeneratedFile>> {
671 let extension_name = config.php_extension_name();
672 let class_name = extension_name.to_pascal_case();
673
674 let mut content = String::new();
676 content.push_str(&crate::template_env::render(
677 "php_file_header.jinja",
678 minijinja::Value::default(),
679 ));
680 content.push_str(&hash::header(CommentStyle::DoubleSlash));
681 content.push_str(&crate::template_env::render(
682 "php_declare_strict_types.jinja",
683 minijinja::Value::default(),
684 ));
685
686 let namespace = php_autoload_namespace(config);
688
689 content.push_str(&crate::template_env::render(
690 "php_namespace.jinja",
691 context! { namespace => &namespace },
692 ));
693 content.push_str(&crate::template_env::render(
694 "php_facade_class_declaration.jinja",
695 context! { class_name => &class_name },
696 ));
697
698 let bridge_param_names_pub: ahash::AHashSet<&str> = config
700 .trait_bridges
701 .iter()
702 .filter_map(|b| b.param_name.as_deref())
703 .collect();
704
705 let no_arg_constructor_types: AHashSet<String> = api
710 .types
711 .iter()
712 .filter(|t| t.fields.iter().all(|f| f.optional))
713 .map(|t| t.name.clone())
714 .collect();
715
716 for func in &api.functions {
718 let method_name = func.name.to_lower_camel_case();
723 let return_php_type = php_type(&func.return_type);
724
725 let visible_params: Vec<_> = func
727 .params
728 .iter()
729 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
730 .collect();
731
732 content.push_str(&crate::template_env::render(
734 "php_phpdoc_block_start.jinja",
735 minijinja::Value::default(),
736 ));
737 if func.doc.is_empty() {
738 content.push_str(&crate::template_env::render(
739 "php_phpdoc_text_line.jinja",
740 context! { text => &format!("{}.", method_name) },
741 ));
742 } else {
743 content.push_str(&crate::template_env::render(
744 "php_phpdoc_lines.jinja",
745 context! {
746 doc_lines => func.doc.lines().collect::<Vec<_>>(),
747 indent => " ",
748 },
749 ));
750 }
751 content.push_str(&crate::template_env::render(
752 "php_phpdoc_empty_line.jinja",
753 minijinja::Value::default(),
754 ));
755 for p in &visible_params {
756 let ptype = php_phpdoc_type(&p.ty);
757 let nullable_prefix = if p.optional { "?" } else { "" };
758 content.push_str(&crate::template_env::render(
759 "php_phpdoc_param_line.jinja",
760 context! {
761 nullable_prefix => nullable_prefix,
762 param_type => &ptype,
763 param_name => &p.name,
764 },
765 ));
766 }
767 let return_phpdoc = php_phpdoc_type(&func.return_type);
768 content.push_str(&crate::template_env::render(
769 "php_phpdoc_return_line.jinja",
770 context! { return_type => &return_phpdoc },
771 ));
772 if func.error_type.is_some() {
773 content.push_str(&crate::template_env::render(
774 "php_phpdoc_throws_line.jinja",
775 context! {
776 namespace => namespace.as_str(),
777 class_name => &class_name,
778 },
779 ));
780 }
781 content.push_str(&crate::template_env::render(
782 "php_phpdoc_block_end.jinja",
783 minijinja::Value::default(),
784 ));
785
786 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
797 if let TypeRef::Named(name) = &p.ty {
798 (name.ends_with("Config") || name.as_str() == "config")
799 && no_arg_constructor_types.contains(name.as_str())
800 } else {
801 false
802 }
803 };
804
805 let mut first_optional_idx = None;
806 for (idx, p) in visible_params.iter().enumerate() {
807 if p.optional || is_optional_config_param(p) {
808 first_optional_idx = Some(idx);
809 break;
810 }
811 }
812
813 content.push_str(&crate::template_env::render(
814 "php_method_signature_start.jinja",
815 context! { method_name => &method_name },
816 ));
817
818 let params: Vec<String> = visible_params
819 .iter()
820 .enumerate()
821 .map(|(idx, p)| {
822 let ptype = php_type(&p.ty);
823 let should_be_optional = p.optional
828 || is_optional_config_param(p)
829 || first_optional_idx.is_some_and(|first| idx >= first);
830 if should_be_optional {
831 format!("?{} ${} = null", ptype, p.name)
832 } else {
833 format!("{} ${}", ptype, p.name)
834 }
835 })
836 .collect();
837 content.push_str(¶ms.join(", "));
838 content.push_str(&crate::template_env::render(
839 "php_method_signature_end.jinja",
840 context! { return_type => &return_php_type },
841 ));
842 let ext_method_name = func.name.to_lower_camel_case();
847 let is_void = matches!(&func.return_type, TypeRef::Unit);
848 let call_params = visible_params
856 .iter()
857 .enumerate()
858 .map(|(idx, p)| {
859 let should_be_optional = p.optional
860 || is_optional_config_param(p)
861 || first_optional_idx.is_some_and(|first| idx >= first);
862 if should_be_optional && is_optional_config_param(p) {
863 if let TypeRef::Named(type_name) = &p.ty {
864 return format!("${} ?? new {}()", p.name, type_name);
865 }
866 }
867 format!("${}", p.name)
868 })
869 .collect::<Vec<_>>()
870 .join(", ");
871 let call_expr = format!("\\{namespace}\\{class_name}Api::{ext_method_name}({call_params})");
872 if is_void {
873 content.push_str(&crate::template_env::render(
874 "php_method_call_statement.jinja",
875 context! { call_expr => &call_expr },
876 ));
877 } else {
878 content.push_str(&crate::template_env::render(
879 "php_method_call_return.jinja",
880 context! { call_expr => &call_expr },
881 ));
882 }
883 content.push_str(&crate::template_env::render(
884 "php_method_end.jinja",
885 minijinja::Value::default(),
886 ));
887 }
888
889 content.push_str(&crate::template_env::render(
890 "php_class_end.jinja",
891 minijinja::Value::default(),
892 ));
893
894 let output_dir = config
898 .php
899 .as_ref()
900 .and_then(|p| p.stubs.as_ref())
901 .map(|s| s.output.to_string_lossy().to_string())
902 .unwrap_or_else(|| "packages/php/src/".to_string());
903
904 let mut files: Vec<GeneratedFile> = Vec::new();
905 files.push(GeneratedFile {
906 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
907 content,
908 generated_header: false,
909 });
910
911 for typ in api.types.iter().filter(|t| t.is_opaque && !t.is_trait) {
917 let opaque_file = gen_php_opaque_class_file(typ, &namespace);
918 files.push(GeneratedFile {
919 path: PathBuf::from(&output_dir).join(format!("{}.php", typ.name)),
920 content: opaque_file,
921 generated_header: false,
922 });
923 }
924
925 Ok(files)
926 }
927
928 fn generate_type_stubs(
929 &self,
930 api: &ApiSurface,
931 config: &ResolvedCrateConfig,
932 ) -> anyhow::Result<Vec<GeneratedFile>> {
933 let extension_name = config.php_extension_name();
934 let class_name = extension_name.to_pascal_case();
935
936 let namespace = php_autoload_namespace(config);
938
939 let mut content = String::new();
944 content.push_str(&crate::template_env::render(
945 "php_file_header.jinja",
946 minijinja::Value::default(),
947 ));
948 content.push_str(&hash::header(CommentStyle::DoubleSlash));
949 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
950 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
951 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
952 content.push_str(&crate::template_env::render(
953 "php_declare_strict_types.jinja",
954 minijinja::Value::default(),
955 ));
956 content.push_str(&crate::template_env::render(
958 "php_namespace_block_begin.jinja",
959 context! { namespace => &namespace },
960 ));
961
962 content.push_str(&crate::template_env::render(
964 "php_exception_class_declaration.jinja",
965 context! { class_name => &class_name },
966 ));
967 content.push_str(
968 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
969 );
970 content.push_str("}\n\n");
971
972 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
979 if typ.is_opaque || typ.fields.is_empty() {
980 continue;
981 }
982 if !typ.doc.is_empty() {
983 content.push_str("/**\n");
984 content.push_str(&crate::template_env::render(
985 "php_phpdoc_lines.jinja",
986 context! {
987 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
988 indent => "",
989 },
990 ));
991 content.push_str(" */\n");
992 }
993 content.push_str(&crate::template_env::render(
994 "php_record_class_stub_declaration.jinja",
995 context! { class_name => &typ.name },
996 ));
997
998 for field in &typ.fields {
1000 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
1001 let prop_type = if field.optional {
1002 let inner = php_type(&field.ty);
1003 if inner.starts_with('?') {
1004 inner
1005 } else {
1006 format!("?{inner}")
1007 }
1008 } else {
1009 php_type(&field.ty)
1010 };
1011 if is_array {
1012 let phpdoc = php_phpdoc_type(&field.ty);
1013 let nullable_prefix = if field.optional { "?" } else { "" };
1014 content.push_str(&crate::template_env::render(
1015 "php_property_type_annotation.jinja",
1016 context! {
1017 nullable_prefix => nullable_prefix,
1018 phpdoc => &phpdoc,
1019 },
1020 ));
1021 }
1022 content.push_str(&crate::template_env::render(
1023 "php_property_stub.jinja",
1024 context! {
1025 prop_type => &prop_type,
1026 field_name => &field.name,
1027 },
1028 ));
1029 }
1030 content.push('\n');
1031
1032 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
1036 sorted_fields.sort_by_key(|f| f.optional);
1037
1038 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
1041 .iter()
1042 .copied()
1043 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
1044 .collect();
1045 if !array_fields.is_empty() {
1046 content.push_str(" /**\n");
1047 for f in &array_fields {
1048 let phpdoc = php_phpdoc_type(&f.ty);
1049 let nullable_prefix = if f.optional { "?" } else { "" };
1050 content.push_str(&crate::template_env::render(
1051 "php_phpdoc_array_param.jinja",
1052 context! {
1053 nullable_prefix => nullable_prefix,
1054 phpdoc => &phpdoc,
1055 param_name => &f.name,
1056 },
1057 ));
1058 }
1059 content.push_str(" */\n");
1060 }
1061
1062 let params: Vec<String> = sorted_fields
1063 .iter()
1064 .map(|f| {
1065 let ptype = php_type(&f.ty);
1066 let nullable = if f.optional && !ptype.starts_with('?') {
1067 format!("?{ptype}")
1068 } else {
1069 ptype
1070 };
1071 let default = if f.optional { " = null" } else { "" };
1072 format!(" {} ${}{}", nullable, f.name, default)
1073 })
1074 .collect();
1075 content.push_str(&crate::template_env::render(
1076 "php_constructor_method.jinja",
1077 context! { params => ¶ms.join(",\n") },
1078 ));
1079
1080 for field in &typ.fields {
1082 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
1083 let return_type = if field.optional {
1084 let inner = php_type(&field.ty);
1085 if inner.starts_with('?') {
1086 inner
1087 } else {
1088 format!("?{inner}")
1089 }
1090 } else {
1091 php_type(&field.ty)
1092 };
1093 let getter_name = field.name.to_lower_camel_case();
1094 if is_array {
1096 let phpdoc = php_phpdoc_type(&field.ty);
1097 let nullable_prefix = if field.optional { "?" } else { "" };
1098 content.push_str(&crate::template_env::render(
1099 "php_constructor_doc_return.jinja",
1100 context! { return_type => &format!("{nullable_prefix}{phpdoc}") },
1101 ));
1102 }
1103 let is_void_getter = return_type == "void";
1104 let getter_body = if is_void_getter {
1105 "{ }".to_string()
1106 } else {
1107 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1108 };
1109 content.push_str(&crate::template_env::render(
1110 "php_getter_stub.jinja",
1111 context! {
1112 getter_name => &format!("get{}", getter_name.to_pascal_case()),
1113 return_type => &return_type,
1114 getter_body => &getter_body,
1115 },
1116 ));
1117 }
1118
1119 content.push_str("}\n\n");
1120 }
1121
1122 for enum_def in &api.enums {
1125 if is_tagged_data_enum(enum_def) {
1126 if !enum_def.doc.is_empty() {
1128 content.push_str("/**\n");
1129 content.push_str(&crate::template_env::render(
1130 "php_phpdoc_lines.jinja",
1131 context! {
1132 doc_lines => enum_def.doc.lines().collect::<Vec<_>>(),
1133 indent => "",
1134 },
1135 ));
1136 content.push_str(" */\n");
1137 }
1138 content.push_str(&crate::template_env::render(
1139 "php_record_class_stub_declaration.jinja",
1140 context! { class_name => &enum_def.name },
1141 ));
1142 content.push_str("}\n\n");
1143 } else {
1144 content.push_str(&crate::template_env::render(
1146 "php_tagged_enum_declaration.jinja",
1147 context! { enum_name => &enum_def.name },
1148 ));
1149 for variant in &enum_def.variants {
1150 let case_name = sanitize_php_enum_case(&variant.name);
1151 content.push_str(&crate::template_env::render(
1152 "php_enum_variant_stub.jinja",
1153 context! {
1154 variant_name => case_name,
1155 value => &variant.name,
1156 },
1157 ));
1158 }
1159 content.push_str("}\n\n");
1160 }
1161 }
1162
1163 if !api.functions.is_empty() {
1168 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1170 .trait_bridges
1171 .iter()
1172 .filter_map(|b| b.param_name.as_deref())
1173 .collect();
1174
1175 content.push_str(&crate::template_env::render(
1176 "php_api_class_declaration.jinja",
1177 context! { class_name => &class_name },
1178 ));
1179 for func in &api.functions {
1180 let return_type = php_type_fq(&func.return_type, &namespace);
1181 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1182 let visible_params: Vec<_> = func
1184 .params
1185 .iter()
1186 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1187 .collect();
1188 let has_array_params = visible_params
1195 .iter()
1196 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1197 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1198 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1199 if has_array_params || has_array_return {
1200 content.push_str(" /**\n");
1201 for p in &visible_params {
1202 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1203 let nullable_prefix = if p.optional { "?" } else { "" };
1204 content.push_str(&crate::template_env::render(
1205 "php_phpdoc_static_param.jinja",
1206 context! {
1207 nullable_prefix => nullable_prefix,
1208 ptype => &ptype,
1209 param_name => &p.name,
1210 },
1211 ));
1212 }
1213 content.push_str(&crate::template_env::render(
1214 "php_phpdoc_static_return.jinja",
1215 context! { return_phpdoc => &return_phpdoc },
1216 ));
1217 content.push_str(" */\n");
1218 }
1219 let params: Vec<String> = visible_params
1220 .iter()
1221 .map(|p| {
1222 let ptype = php_type_fq(&p.ty, &namespace);
1223 if p.optional {
1224 format!("?{} ${} = null", ptype, p.name)
1225 } else {
1226 format!("{} ${}", ptype, p.name)
1227 }
1228 })
1229 .collect();
1230 let stub_method_name = func.name.to_lower_camel_case();
1234 let is_void_stub = return_type == "void";
1235 let stub_body = if is_void_stub {
1236 "{ }".to_string()
1237 } else {
1238 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1239 };
1240 content.push_str(&crate::template_env::render(
1241 "php_static_method_stub.jinja",
1242 context! {
1243 method_name => &stub_method_name,
1244 params => ¶ms.join(", "),
1245 return_type => &return_type,
1246 stub_body => &stub_body,
1247 },
1248 ));
1249 }
1250 content.push_str("}\n\n");
1251 }
1252
1253 content.push_str(&crate::template_env::render(
1255 "php_namespace_block_end.jinja",
1256 minijinja::Value::default(),
1257 ));
1258
1259 let output_dir = config
1261 .php
1262 .as_ref()
1263 .and_then(|p| p.stubs.as_ref())
1264 .map(|s| s.output.to_string_lossy().to_string())
1265 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1266
1267 Ok(vec![GeneratedFile {
1268 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1269 content,
1270 generated_header: false,
1271 }])
1272 }
1273
1274 fn build_config(&self) -> Option<BuildConfig> {
1275 Some(BuildConfig {
1276 tool: "cargo",
1277 crate_suffix: "-php",
1278 build_dep: BuildDependency::None,
1279 post_build: vec![],
1280 })
1281 }
1282}
1283
1284fn php_phpdoc_type(ty: &TypeRef) -> String {
1287 match ty {
1288 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1289 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1290 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1291 _ => php_type(ty),
1292 }
1293}
1294
1295fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1297 match ty {
1298 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1299 TypeRef::Map(k, v) => format!(
1300 "array<{}, {}>",
1301 php_phpdoc_type_fq(k, namespace),
1302 php_phpdoc_type_fq(v, namespace)
1303 ),
1304 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1305 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1306 _ => php_type(ty),
1307 }
1308}
1309
1310fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1312 match ty {
1313 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1314 TypeRef::Optional(inner) => {
1315 let inner_type = php_type_fq(inner, namespace);
1316 if inner_type.starts_with('?') {
1317 inner_type
1318 } else {
1319 format!("?{inner_type}")
1320 }
1321 }
1322 _ => php_type(ty),
1323 }
1324}
1325
1326fn gen_php_opaque_class_file(typ: &alef_core::ir::TypeDef, namespace: &str) -> String {
1333 let mut content = String::new();
1334 content.push_str(&crate::template_env::render(
1335 "php_file_header.jinja",
1336 minijinja::Value::default(),
1337 ));
1338 content.push_str(&hash::header(CommentStyle::DoubleSlash));
1339 content.push_str(&crate::template_env::render(
1340 "php_declare_strict_types.jinja",
1341 minijinja::Value::default(),
1342 ));
1343 content.push_str(&crate::template_env::render(
1344 "php_namespace.jinja",
1345 context! { namespace => namespace },
1346 ));
1347
1348 if !typ.doc.is_empty() {
1350 content.push_str("/**\n");
1351 content.push_str(&crate::template_env::render(
1352 "php_phpdoc_lines.jinja",
1353 context! {
1354 doc_lines => typ.doc.lines().collect::<Vec<_>>(),
1355 indent => "",
1356 },
1357 ));
1358 content.push_str(" */\n");
1359 }
1360
1361 content.push_str(&format!("final class {}\n{{\n", typ.name));
1362
1363 let mut method_order: Vec<&alef_core::ir::MethodDef> = Vec::new();
1365 method_order.extend(typ.methods.iter().filter(|m| m.receiver.is_some()));
1366 method_order.extend(typ.methods.iter().filter(|m| m.receiver.is_none()));
1367
1368 for method in method_order {
1369 let method_name = method.name.to_lower_camel_case();
1370 let return_type = php_type(&method.return_type);
1371 let is_void = matches!(&method.return_type, TypeRef::Unit);
1372 let is_static = method.receiver.is_none();
1373
1374 let mut doc_lines: Vec<String> = vec![];
1376 let doc_line = method.doc.lines().next().unwrap_or("").trim();
1377 if !doc_line.is_empty() {
1378 doc_lines.push(doc_line.to_string());
1379 }
1380
1381 let mut phpdoc_params: Vec<String> = vec![];
1383 for param in &method.params {
1384 if matches!(¶m.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)) {
1385 let phpdoc_type = php_phpdoc_type(¶m.ty);
1386 phpdoc_params.push(format!("@param {} ${}", phpdoc_type, param.name));
1387 }
1388 }
1389 doc_lines.extend(phpdoc_params);
1390
1391 let needs_return_phpdoc = matches!(&method.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _));
1393 if needs_return_phpdoc {
1394 let phpdoc_type = php_phpdoc_type(&method.return_type);
1395 doc_lines.push(format!("@return {phpdoc_type}"));
1396 }
1397
1398 if !doc_lines.is_empty() {
1400 content.push_str(" /**\n");
1401 for line in doc_lines {
1402 content.push_str(&format!(" * {}\n", line));
1403 }
1404 content.push_str(" */\n");
1405 }
1406
1407 let static_kw = if is_static { "static " } else { "" };
1409 let params: Vec<String> = method
1410 .params
1411 .iter()
1412 .map(|p| {
1413 let ptype = php_type(&p.ty);
1414 if p.optional {
1415 format!("?{} ${} = null", ptype, p.name)
1416 } else {
1417 format!("{} ${}", ptype, p.name)
1418 }
1419 })
1420 .collect();
1421 content.push_str(&format!(
1422 " public {static_kw}function {method_name}({}): {return_type}\n",
1423 params.join(", ")
1424 ));
1425 let body = if is_void {
1426 " {\n }\n"
1427 } else {
1428 " {\n throw new \\RuntimeException('Not implemented — provided by the native extension.');\n }\n"
1429 };
1430 content.push_str(body);
1431 }
1432
1433 content.push_str("}\n");
1434 content
1435}
1436
1437fn php_type(ty: &TypeRef) -> String {
1439 match ty {
1440 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1441 TypeRef::Primitive(p) => match p {
1442 PrimitiveType::Bool => "bool".to_string(),
1443 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1444 PrimitiveType::U8
1445 | PrimitiveType::U16
1446 | PrimitiveType::U32
1447 | PrimitiveType::U64
1448 | PrimitiveType::I8
1449 | PrimitiveType::I16
1450 | PrimitiveType::I32
1451 | PrimitiveType::I64
1452 | PrimitiveType::Usize
1453 | PrimitiveType::Isize => "int".to_string(),
1454 },
1455 TypeRef::Optional(inner) => {
1456 let inner_type = php_type(inner);
1459 if inner_type.starts_with('?') {
1460 inner_type
1461 } else {
1462 format!("?{inner_type}")
1463 }
1464 }
1465 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1466 TypeRef::Named(name) => name.clone(),
1467 TypeRef::Unit => "void".to_string(),
1468 TypeRef::Duration => "float".to_string(),
1469 }
1470}