use super::ProtobufGenerator;
use super::base::{escape_string, map_proto_type_to_language, sanitize_identifier, to_camel_case};
use crate::codegen::protobuf::spec_parser::{FieldLabel, MessageDef, ProtobufSchema, ServiceDef};
use anyhow::Result;
#[derive(Default, Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct PhpProtobufGenerator;
impl ProtobufGenerator for PhpProtobufGenerator {
fn generate_complete(&self, schema: &ProtobufSchema) -> Result<String> {
let mut code = String::new();
let namespace = php_namespace(schema.package.as_deref());
let has_messages = !schema.messages.is_empty() || !schema.enums.is_empty();
let has_services = !schema.services.is_empty();
code.push_str("<?php\n");
code.push_str("// DO NOT EDIT - Auto-generated by Spikard CLI\n");
code.push_str("//\n");
code.push_str("// This file was automatically generated from your Protobuf schema.\n");
code.push_str("// Any manual changes will be overwritten on the next generation.\n\n");
code.push_str("declare(strict_types=1);\n\n");
code.push_str(&format!("namespace {namespace};\n\n"));
if has_messages {
code.push_str("use Google\\Protobuf\\Internal\\Message;\n");
}
if has_services {
code.push_str("use RuntimeException;\n");
}
if has_messages || has_services {
code.push('\n');
}
for message in schema.messages.values() {
code.push_str(&self.generate_message_class(message));
code.push_str("\n\n");
}
for enum_def in schema.enums.values() {
code.push_str(&self.generate_enum_class(enum_def));
code.push_str("\n\n");
}
if schema.services.is_empty() {
code.push_str("// No services defined in this schema.\n");
} else {
for service in schema.services.values() {
code.push_str(&self.generate_service_class(service));
code.push_str("\n\n");
}
}
Ok(code.trim_end().to_string() + "\n")
}
fn generate_messages(&self, schema: &ProtobufSchema) -> Result<String> {
let mut code = String::new();
let namespace = php_namespace(schema.package.as_deref());
code.push_str("<?php\n");
code.push_str("// DO NOT EDIT - Auto-generated by Spikard CLI\n");
code.push_str("//\n");
code.push_str("// This file was automatically generated from your Protobuf schema.\n");
code.push_str("// Any manual changes will be overwritten on the next generation.\n\n");
code.push_str("declare(strict_types=1);\n\n");
code.push_str(&format!("namespace {namespace};\n\n"));
code.push_str("use Google\\Protobuf\\Internal\\Message;\n\n");
for message in schema.messages.values() {
code.push_str(&self.generate_message_class(message));
code.push_str("\n\n");
}
for enum_def in schema.enums.values() {
code.push_str(&self.generate_enum_class(enum_def));
code.push_str("\n\n");
}
Ok(code.trim_end().to_string() + "\n")
}
fn generate_services(&self, schema: &ProtobufSchema) -> Result<String> {
let mut code = String::new();
let namespace = php_namespace(schema.package.as_deref());
code.push_str("<?php\n");
code.push_str("// DO NOT EDIT - Auto-generated by Spikard CLI\n");
code.push_str("//\n");
code.push_str("// This file was automatically generated from your Protobuf schema.\n");
code.push_str("// Any manual changes will be overwritten on the next generation.\n\n");
code.push_str("declare(strict_types=1);\n\n");
code.push_str(&format!("namespace {namespace};\n\n"));
code.push_str("use RuntimeException;\n\n");
if schema.services.is_empty() {
code.push_str("// No services defined in this schema.\n");
} else {
for service in schema.services.values() {
code.push_str(&self.generate_service_class(service));
code.push_str("\n\n");
}
}
Ok(code.trim_end().to_string() + "\n")
}
}
fn php_namespace(package: Option<&str>) -> String {
package.unwrap_or("Protobuf").replace('.', "\\")
}
impl PhpProtobufGenerator {
fn phpdoc_field_type(&self, field: &crate::codegen::protobuf::spec_parser::FieldDef) -> String {
let base_type = map_proto_type_to_language(&field.field_type, "php", false, false);
match field.label {
FieldLabel::Repeated => format!("list<{base_type}>"),
FieldLabel::Optional => format!("{base_type}|null"),
_ => base_type,
}
}
#[allow(dead_code)]
fn generate_message_class(&self, message: &MessageDef) -> String {
let mut code = String::new();
code.push_str(&format!("class {} extends Message\n", message.name));
code.push_str("{\n");
if let Some(desc) = &message.description {
code.push_str(&format!(" /**\n * {}\n */\n", escape_string(desc, "php")));
} else {
code.push_str(" /**\n * Generated protocol buffer message.\n */\n");
}
if message.fields.is_empty() {
code.push_str("}\n");
} else {
for field in &message.fields {
if let Some(desc) = &field.description {
code.push_str(&format!(" /**\n * {}\n */\n", escape_string(desc, "php")));
}
let field_name = sanitize_identifier(&field.name, "php");
let is_optional = field.label == FieldLabel::Optional;
let is_repeated = field.label == FieldLabel::Repeated;
let field_type = map_proto_type_to_language(&field.field_type, "php", is_optional, is_repeated);
let phpdoc_type = self.phpdoc_field_type(field);
let default_val = if is_repeated {
"[]".to_string()
} else if is_optional {
"null".to_string()
} else if matches!(
field.field_type,
crate::codegen::protobuf::spec_parser::ProtoType::String
| crate::codegen::protobuf::spec_parser::ProtoType::Bytes
) {
"''".to_string()
} else if matches!(field.field_type, crate::codegen::protobuf::spec_parser::ProtoType::Bool) {
"false".to_string()
} else {
"0".to_string()
};
code.push_str(&format!(" /** @var {phpdoc_type} */\n"));
code.push_str(&format!(" protected {field_type} ${field_name} = {default_val};\n"));
}
code.push_str("}\n");
}
code
}
#[allow(dead_code)]
fn generate_enum_class(&self, enum_def: &crate::codegen::protobuf::spec_parser::EnumDef) -> String {
let mut code = String::new();
code.push_str(&format!("class {} extends Message\n", enum_def.name));
code.push_str("{\n");
if let Some(desc) = &enum_def.description {
code.push_str(&format!(" /**\n * {}\n */\n", escape_string(desc, "php")));
} else {
code.push_str(" /**\n * Protobuf enum type.\n */\n");
}
if enum_def.values.is_empty() {
code.push_str("}\n");
} else {
for value in &enum_def.values {
if let Some(desc) = &value.description {
code.push_str(&format!(" /** {} */\n", escape_string(desc, "php")));
}
code.push_str(&format!(
" const {} = {};\n",
value.name.to_uppercase(),
value.number
));
}
code.push_str("}\n");
}
code
}
#[allow(dead_code)]
fn generate_service_class(&self, service: &ServiceDef) -> String {
let mut code = String::new();
code.push_str(&format!("class {}\n", service.name));
code.push_str("{\n");
if let Some(desc) = &service.description {
code.push_str(&format!(
" /**\n * Server handler interface for {}.\n * {}\n */\n",
service.name,
escape_string(desc, "php")
));
} else {
code.push_str(&format!(
" /**\n * Server handler interface for {}.\n */\n",
service.name
));
}
if service.methods.is_empty() {
code.push_str("}\n");
} else {
for method in &service.methods {
code.push('\n');
if let Some(desc) = &method.description {
code.push_str(&format!(" /**\n * {}\n", escape_string(desc, "php")));
} else {
code.push_str(" /**\n");
}
let sanitized_name = sanitize_identifier(&method.name, "php");
let method_name = to_camel_case(&sanitized_name);
let request_type = &method.input_type;
let response_type = &method.output_type;
code.push_str(&format!(" * @param {request_type} $request\n"));
code.push_str(&format!(" * @return {response_type}\n"));
code.push_str(" */\n");
code.push_str(&format!(
" public function {method_name}({request_type} $request): {response_type}\n"
));
code.push_str(" {\n");
code.push_str(" throw new RuntimeException('Not implemented');\n");
code.push_str(" }\n");
}
code.push_str("}\n");
}
code
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::protobuf::spec_parser::{FieldDef, FieldLabel, MessageDef, ProtoType};
#[test]
fn test_generate_simple_message() {
let message = MessageDef {
name: "User".to_string(),
fields: vec![
FieldDef {
name: "id".to_string(),
number: 1,
field_type: ProtoType::String,
label: FieldLabel::None,
default_value: None,
description: None,
},
FieldDef {
name: "name".to_string(),
number: 2,
field_type: ProtoType::String,
label: FieldLabel::None,
default_value: None,
description: Some("User's full name".to_string()),
},
],
nested_messages: std::collections::HashMap::new(),
nested_enums: std::collections::HashMap::new(),
description: Some("Represents a user".to_string()),
};
let generator = PhpProtobufGenerator;
let code = generator.generate_message_class(&message);
assert!(code.contains("class User extends Message"));
assert!(code.contains("Represents a user"));
assert!(code.contains("protected string $id = ''"));
assert!(code.contains("protected string $name = ''"));
}
#[test]
fn test_generate_message_with_optional_field() {
let message = MessageDef {
name: "User".to_string(),
fields: vec![FieldDef {
name: "email".to_string(),
number: 3,
field_type: ProtoType::String,
label: FieldLabel::Optional,
default_value: None,
description: None,
}],
nested_messages: std::collections::HashMap::new(),
nested_enums: std::collections::HashMap::new(),
description: None,
};
let generator = PhpProtobufGenerator;
let code = generator.generate_message_class(&message);
assert!(code.contains("protected ?string $email = null"));
}
#[test]
fn test_generate_message_with_repeated_field() {
let message = MessageDef {
name: "User".to_string(),
fields: vec![FieldDef {
name: "tags".to_string(),
number: 4,
field_type: ProtoType::String,
label: FieldLabel::Repeated,
default_value: None,
description: None,
}],
nested_messages: std::collections::HashMap::new(),
nested_enums: std::collections::HashMap::new(),
description: None,
};
let generator = PhpProtobufGenerator;
let code = generator.generate_message_class(&message);
assert!(code.contains("protected array $tags = []"));
}
#[test]
fn test_generate_typed_properties() {
let message = MessageDef {
name: "TestMessage".to_string(),
fields: vec![
FieldDef {
name: "count".to_string(),
number: 1,
field_type: ProtoType::Int32,
label: FieldLabel::None,
default_value: None,
description: None,
},
FieldDef {
name: "enabled".to_string(),
number: 2,
field_type: ProtoType::Bool,
label: FieldLabel::None,
default_value: None,
description: None,
},
FieldDef {
name: "value".to_string(),
number: 3,
field_type: ProtoType::Float,
label: FieldLabel::None,
default_value: None,
description: None,
},
],
nested_messages: std::collections::HashMap::new(),
nested_enums: std::collections::HashMap::new(),
description: None,
};
let generator = PhpProtobufGenerator;
let code = generator.generate_message_class(&message);
assert!(code.contains("protected int $count = 0"));
assert!(code.contains("protected bool $enabled = false"));
assert!(code.contains("protected float $value = 0"));
}
#[test]
fn test_generate_service_class() {
let service = ServiceDef {
name: "UserService".to_string(),
methods: vec![crate::codegen::protobuf::spec_parser::MethodDef {
name: "get_user".to_string(),
input_type: "GetUserRequest".to_string(),
output_type: "User".to_string(),
input_streaming: false,
output_streaming: false,
description: None,
}],
description: Some("User service".to_string()),
};
let generator = PhpProtobufGenerator;
let code = generator.generate_service_class(&service);
assert!(code.contains("class UserService"));
assert!(code.contains("public function getUser"));
assert!(code.contains("GetUserRequest"));
assert!(code.contains("User"));
assert!(code.contains("RuntimeException"));
}
#[test]
fn test_generate_messages_with_namespace() {
let schema = ProtobufSchema {
package: Some("Example\\Api".to_string()),
messages: vec![(
"User".to_string(),
MessageDef {
name: "User".to_string(),
fields: vec![FieldDef {
name: "id".to_string(),
number: 1,
field_type: ProtoType::String,
label: FieldLabel::None,
default_value: None,
description: None,
}],
nested_messages: std::collections::HashMap::new(),
nested_enums: std::collections::HashMap::new(),
description: None,
},
)]
.into_iter()
.collect(),
services: std::collections::HashMap::new(),
enums: std::collections::HashMap::new(),
imports: vec![],
syntax: "proto3".to_string(),
description: None,
};
let generator = PhpProtobufGenerator;
let code = generator
.generate_messages(&schema)
.expect("Failed to generate messages");
assert!(code.contains("<?php"));
assert!(code.contains("namespace Example\\Api"));
assert!(code.contains("use Google\\Protobuf\\Internal\\Message"));
assert!(code.contains("class User extends Message"));
}
#[test]
fn test_generate_services_with_namespace() {
let schema = ProtobufSchema {
package: Some("Example\\Service".to_string()),
messages: std::collections::HashMap::new(),
services: vec![(
"UserService".to_string(),
ServiceDef {
name: "UserService".to_string(),
methods: vec![],
description: None,
},
)]
.into_iter()
.collect(),
enums: std::collections::HashMap::new(),
imports: vec![],
syntax: "proto3".to_string(),
description: None,
};
let generator = PhpProtobufGenerator;
let code = generator
.generate_services(&schema)
.expect("Failed to generate services");
assert!(code.contains("<?php"));
assert!(code.contains("namespace Example\\Service"));
assert!(code.contains("use RuntimeException"));
}
#[test]
fn test_enum_class_generation() {
let enum_def = crate::codegen::protobuf::spec_parser::EnumDef {
name: "Status".to_string(),
values: vec![
crate::codegen::protobuf::spec_parser::EnumValue {
name: "UNKNOWN".to_string(),
number: 0,
description: None,
},
crate::codegen::protobuf::spec_parser::EnumValue {
name: "ACTIVE".to_string(),
number: 1,
description: Some("Active status".to_string()),
},
],
description: Some("Status enumeration".to_string()),
};
let generator = PhpProtobufGenerator;
let code = generator.generate_enum_class(&enum_def);
assert!(code.contains("class Status extends Message"));
assert!(code.contains("const UNKNOWN = 0"));
assert!(code.contains("const ACTIVE = 1"));
assert!(code.contains("Status enumeration"));
}
}