diff --git a/Cargo.lock b/Cargo.lock index ef1189b9..5692330f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1003,6 +1003,7 @@ dependencies = [ "phper-build", "phper-macros", "phper-sys", + "sealed", "thiserror 2.0.11", ] @@ -1026,6 +1027,7 @@ name = "phper-doc" version = "0.15.1" dependencies = [ "phper", + "phper-build", "reqwest", "thiserror 2.0.11", ] @@ -1365,6 +1367,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sealed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "security-framework" version = "2.11.1" diff --git a/phper-build/src/lib.rs b/phper-build/src/lib.rs index 76b440bf..8563e175 100644 --- a/phper-build/src/lib.rs +++ b/phper-build/src/lib.rs @@ -24,24 +24,28 @@ pub fn register_all() { pub fn register_configures() { // versions println!( - "cargo:rustc-cfg=phper_major_version=\"{}\"", + "cargo::rustc-cfg=phper_major_version=\"{}\"", PHP_MAJOR_VERSION ); println!( - "cargo:rustc-cfg=phper_minor_version=\"{}\"", + "cargo::rustc-cfg=phper_minor_version=\"{}\"", PHP_MINOR_VERSION ); println!( - "cargo:rustc-cfg=phper_release_version=\"{}\"", + "cargo::rustc-cfg=phper_release_version=\"{}\"", PHP_RELEASE_VERSION ); if PHP_DEBUG != 0 { - println!("cargo:rustc-cfg=phper_debug"); + println!("cargo::rustc-cfg=phper_debug"); } if USING_ZTS != 0 { - println!("cargo:rustc-cfg=phper_zts"); + println!("cargo::rustc-cfg=phper_zts"); + } + + if PHP_VERSION_ID >= 80100 { + println!("cargo::rustc-cfg=phper_enum_supported"); } } @@ -49,7 +53,7 @@ pub fn register_configures() { pub fn register_link_args() { #[cfg(target_os = "macos")] { - println!("cargo:rustc-link-arg=-undefined"); - println!("cargo:rustc-link-arg=dynamic_lookup"); + println!("cargo::rustc-link-arg=-undefined"); + println!("cargo::rustc-link-arg=dynamic_lookup"); } } diff --git a/phper-doc/Cargo.toml b/phper-doc/Cargo.toml index 983d5153..fb9a983a 100644 --- a/phper-doc/Cargo.toml +++ b/phper-doc/Cargo.toml @@ -25,3 +25,11 @@ phper = { workspace = true } [dev-dependencies] thiserror = "2.0.11" reqwest = { version = "0.12.12", features = ["blocking", "cookies"] } + +[build-dependencies] +phper-build = { workspace = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(phper_enum_supported)', +] } diff --git a/phper-doc/doc/_06_module/_08_register_enum/index.md b/phper-doc/doc/_06_module/_08_register_enum/index.md new file mode 100644 index 00000000..8b109b10 --- /dev/null +++ b/phper-doc/doc/_06_module/_08_register_enum/index.md @@ -0,0 +1,273 @@ +# Register Enums + +> PHP 8.1 and above introduced the Enum functionality. `PHPER` allowing you to create PHP enums using Rust code. + +In `PHPER`, you can use the [`add_enum`](phper::modules::Module::add_enum) method to register enums. +According to PHP's enum specification, `PHPER` supports three types of enums: + +1. Pure enums (without values) +2. Integer-backed enums +3. String-backed enums + +## Creating Pure Enums + +Pure enums are the simplest type of enum, having only member names without associated values. Use `EnumEntity<()>` to create a pure enum (or simply use `EnumEntity::new()` since `()` is the default type parameter). + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity, classes::Visibility}; + +#[php_get_module] +pub fn get_module() -> Module { + let mut module = Module::new( + env!("CARGO_CRATE_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_AUTHORS"), + ); + + // Create a pure enum + let mut status = EnumEntity::new("Status"); + + // Add enum cases (without values) + status.add_case("PENDING", ()); + status.add_case("ACTIVE", ()); + status.add_case("INACTIVE", ()); + + // Register the enum to the module + module.add_enum(status); + + module +} +``` + +This is equivalent to the following PHP code: + +```php +enum Status { + case PENDING; + case ACTIVE; + case INACTIVE; +} +``` + +## Creating Integer-Backed Enums + +Integer-backed enums associate each enum member with an integer value. Use `EnumEntity` to create an integer-backed enum. + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity}; + +#[php_get_module] +pub fn get_module() -> Module { + let mut module = Module::new( + env!("CARGO_CRATE_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_AUTHORS"), + ); + + // Create an integer-backed enum + let mut level = EnumEntity::::new("Level"); + + // Add enum cases with their associated integer values + level.add_case("LOW", 1); + level.add_case("MEDIUM", 5); + level.add_case("HIGH", 10); + + // Register the enum to the module + module.add_enum(level); + + module +} +``` + +This is equivalent to the following PHP code: + +```php +enum Level: int { + case LOW = 1; + case MEDIUM = 5; + case HIGH = 10; +} +``` + +## Creating String-Backed Enums + +String-backed enums associate each enum member with a string value. Use `EnumEntity` to create a string-backed enum. + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity}; + +#[php_get_module] +pub fn get_module() -> Module { + let mut module = Module::new( + env!("CARGO_CRATE_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_AUTHORS"), + ); + + // Create a string-backed enum + let mut color = EnumEntity::::new("Color"); + + // Add enum cases with their associated string values + color.add_case("RED", "FF0000".to_string()); + color.add_case("GREEN", "00FF00".to_string()); + color.add_case("BLUE", "0000FF".to_string()); + + // Register the enum to the module + module.add_enum(color); + + module +} +``` + +This is equivalent to the following PHP code: + +```php +enum Color: string { + case RED = "FF0000"; + case GREEN = "00FF00"; + case BLUE = "0000FF"; +} +``` + +## Adding Constants + +Enums can contain constants. Use the `add_constant` method to add constants to an enum. + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity}; + +let mut status = EnumEntity::new("Status"); + +// Add enum cases +status.add_case("PENDING", ()); +status.add_case("ACTIVE", ()); + +// Add constants +status.add_constant("VERSION", "1.0.0"); +status.add_constant("MAX_ATTEMPTS", 3); +``` + +This is equivalent to the following PHP code: + +```php +enum Status { + case PENDING; + case ACTIVE; + + public const VERSION = "1.0.0"; + public const MAX_ATTEMPTS = 3; +} +``` + +## Adding Static Methods + +You can add static methods to enums. Use the `add_static_method` method to add a static method to an enum. + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity, classes::Visibility}; +use std::convert::Infallible; + +let mut status = EnumEntity::new("Status"); + +// Add enum cases +status.add_case("PENDING", ()); +status.add_case("ACTIVE", ()); + +// Add static method +status.add_static_method("getDescription", Visibility::Public, |_| { + Ok::<_, Infallible>("Status enumeration for tracking item states") +}); +``` + +This is equivalent to the following PHP code: + +```php +enum Status { + case PENDING; + case ACTIVE; + + public static function getDescription(): string { + return "Status enumeration for tracking item states"; + } +} +``` + +## Implementing Interfaces + +You can make enums implement interfaces. Use the `implements` method to make an enum implement a specific interface. + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity, classes::Interface}; + +let mut color = EnumEntity::::new("Color"); + +// Add enum cases +color.add_case("RED", "FF0000".to_string()); +color.add_case("GREEN", "00FF00".to_string()); + +// Implement interface +color.implements(Interface::from_name("JsonSerializable")); + +// Note: You need to add necessary methods to satisfy interface requirements +// For example, JsonSerializable interface requires the implementation of jsonSerialize method +``` + +## Using Built-in Enum Methods + +PHP enums come with some built-in methods. Pure enums (`UnitEnum`) have the `cases()` method, while backed enums (`BackedEnum`) additionally have the `from()` and `tryFrom()` methods. + +```php +// Examples of using built-in methods in PHP +$allCases = Color::cases(); // Returns an array of all enum cases + +// Only available for backed enums +$colorFromValue = Color::from("FF0000"); // Returns RED +$colorOrNull = Color::tryFrom("INVALID"); // Returns null (when the value doesn't exist) +``` + +## Complete Example + +Here's a comprehensive example using both pure and backed enums: + +```rust,no_run +use phper::{ + modules::Module, + php_get_module, + enums::EnumEntity, + classes::Visibility +}; +use std::convert::Infallible; + +#[php_get_module] +pub fn get_module() -> Module { + let mut module = Module::new( + env!("CARGO_CRATE_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_AUTHORS"), + ); + + // Pure enum + let mut status = EnumEntity::new("Status"); + status.add_case("PENDING", ()); + status.add_case("ACTIVE", ()); + status.add_case("INACTIVE", ()); + status.add_constant("VERSION", "1.0.0"); + status.add_static_method("getDescription", Visibility::Public, |_| { + Ok::<_, Infallible>("Status enumeration") + }); + + // Integer-backed enum + let mut level = EnumEntity::::new("Level"); + level.add_case("LOW", 1); + level.add_case("MEDIUM", 5); + level.add_case("HIGH", 10); + + // Register enums to the module + module.add_enum(status); + module.add_enum(level); + + module +} +``` + +> **Note**: PHP enums require PHP 8.1 or higher. Make sure your extension sets the correct PHP version requirements. diff --git a/phper-doc/src/lib.rs b/phper-doc/src/lib.rs index 24cbadd9..555a8aa3 100644 --- a/phper-doc/src/lib.rs +++ b/phper-doc/src/lib.rs @@ -69,6 +69,10 @@ pub mod _06_module { #[doc = include_str!("../doc/_06_module/_07_register_interface/index.md")] pub mod _07_register_interface {} + + #[cfg(phper_enum_supported)] + #[doc = include_str!("../doc/_06_module/_08_register_enum/index.md")] + pub mod _08_register_enum {} } /// TODO diff --git a/phper-sys/php_wrapper.c b/phper-sys/php_wrapper.c index 5935d7da..555aaeab 100644 --- a/phper-sys/php_wrapper.c +++ b/phper-sys/php_wrapper.c @@ -22,6 +22,10 @@ #include #endif +#if PHP_VERSION_ID >= 80100 +#include +#endif + typedef ZEND_INI_MH(phper_zend_ini_mh); typedef zend_class_entry * diff --git a/phper/Cargo.toml b/phper/Cargo.toml index 6f13cd99..01ee95c1 100644 --- a/phper/Cargo.toml +++ b/phper/Cargo.toml @@ -28,6 +28,7 @@ indexmap = "2.7.1" phper-alloc = { workspace = true } phper-macros = { workspace = true } phper-sys = { workspace = true } +sealed = "0.6.0" thiserror = "2.0.11" [build-dependencies] @@ -44,4 +45,9 @@ unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(phper_minor_version, values("3"))', 'cfg(phper_minor_version, values("4"))', 'cfg(phper_zts)', + 'cfg(phper_enum_supported)', ] } + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/phper/src/classes.rs b/phper/src/classes.rs index 5ce729cc..4dbb769e 100644 --- a/phper/src/classes.rs +++ b/phper/src/classes.rs @@ -233,7 +233,7 @@ fn find_global_class_entry_ptr(name: impl AsRef) -> *mut zend_class_entry { } #[derive(Clone)] -enum InnerClassEntry { +pub(crate) enum InnerClassEntry { Ptr(*const zend_class_entry), Name(String), } @@ -931,7 +931,7 @@ pub struct ConstantEntity { } impl ConstantEntity { - fn new(name: impl Into, value: impl Into) -> Self { + pub(crate) fn new(name: impl Into, value: impl Into) -> Self { Self { name: name.into(), value: value.into(), @@ -940,14 +940,16 @@ impl ConstantEntity { } /// Builder for declare class property. -struct PropertyEntity { +pub(crate) struct PropertyEntity { name: String, visibility: RawVisibility, value: Scalar, } impl PropertyEntity { - fn new(name: impl Into, visibility: Visibility, value: impl Into) -> Self { + pub(crate) fn new( + name: impl Into, visibility: Visibility, value: impl Into, + ) -> Self { Self { name: name.into(), visibility: visibility as RawVisibility, @@ -1027,7 +1029,7 @@ pub enum Visibility { pub(crate) type RawVisibility = u32; #[allow(clippy::useless_conversion)] -unsafe extern "C" fn create_object(ce: *mut zend_class_entry) -> *mut zend_object { +pub(crate) unsafe extern "C" fn create_object(ce: *mut zend_class_entry) -> *mut zend_object { unsafe { // Get real ce which hold state_constructor. let real_ce = find_real_ce(ce).unwrap(); @@ -1146,7 +1148,9 @@ unsafe fn clone_object_common(object: *mut zend_object) -> *mut zend_object { } } -unsafe fn add_class_constant(class_ce: *mut _zend_class_entry, constant: &ConstantEntity) { +pub(crate) unsafe fn add_class_constant( + class_ce: *mut _zend_class_entry, constant: &ConstantEntity, +) { let name_ptr = constant.name.as_ptr() as *const c_char; let name_len = constant.name.len(); unsafe { diff --git a/phper/src/enums.rs b/phper/src/enums.rs new file mode 100644 index 00000000..34a5f06a --- /dev/null +++ b/phper/src/enums.rs @@ -0,0 +1,301 @@ +// Copyright (c) 2022 PHPER Framework Team +// PHPER is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +//! APIs related to PHP enum functionality. +//! +//! This module provides Rust wrappers for PHP enum functionality, allowing you +//! to define and work with PHP enums from Rust code. It supports pure enums, +//! integer-backed enums, and string-backed enums, corresponding to their PHP +//! counterparts. +//! +//! The implementation respects the PHP 8.1+ enum feature set and provides a +//! type-safe interface for creating enum cases and handling enum values. +#![cfg(phper_enum_supported)] + +use crate::{ + classes::{ConstantEntity, Interface, Visibility, add_class_constant}, + errors::Throwable, + functions::{Function, FunctionEntry, HandlerMap, MethodEntity}, + sys::*, + types::Scalar, + utils::ensure_end_with_zero, + values::ZVal, +}; +use sealed::sealed; +use std::{ + ffi::{CStr, CString}, + marker::PhantomData, + mem::{MaybeUninit, zeroed}, + ptr::null_mut, + rc::Rc, +}; + +/// Trait representing a backing type for enum values. +/// +/// This trait is implemented by types that can serve as backing values +/// for PHP enums. The trait is sealed to ensure only supported types +/// can be used as enum backing types. +#[sealed] +pub trait EnumBackingType: Into { + /// Returns the PHP enum type representation for this backing type. + fn enum_type() -> EnumType; +} + +#[sealed] +impl EnumBackingType for () { + fn enum_type() -> EnumType { + EnumType::Pure + } +} + +#[sealed] +impl EnumBackingType for i64 { + fn enum_type() -> EnumType { + EnumType::IntBacked + } +} + +#[sealed] +impl EnumBackingType for String { + fn enum_type() -> EnumType { + EnumType::StringBacked + } +} + +/// Enum type in PHP. +/// +/// Represents the three possible types of PHP enums: +/// - Pure enums (no backing value) +/// - Integer-backed enums +/// - String-backed enums +pub enum EnumType { + /// Pure enum (like `enum Foo { case A, case B }`) + Pure, + /// Int backed enum (like `enum Foo: int { case A = 1, case B = 2 }`) + IntBacked, + /// String backed enum (like `enum Foo: string { case A = 'a', case B = 'b' + /// }`) + StringBacked, +} + +/// Enum case definition for PHP enum. +/// +/// Represents a single case within a PHP enum, storing its name +/// and associated value. +struct EnumCase { + name: CString, + value: Scalar, +} + +/// Builder for registering a PHP enum. +/// +/// This struct facilitates the creation and registration of PHP enums from Rust +/// code. The generic parameter B represents the backing type and determines the +/// enum type. +/// +/// # Type Parameters +/// +/// * `B` - A type that implements `EnumBackingType`, determining the enum's +/// backing type. Use `()` for pure enums, `i64` for int-backed enums, or +/// `String` for string-backed enums. +pub struct EnumEntity { + enum_name: CString, + enum_type: EnumType, + method_entities: Vec, + cases: Vec, + constants: Vec, + interfaces: Vec, + _p: PhantomData<(B, *mut ())>, +} + +impl EnumEntity { + /// Creates a new enum builder with the specified name. + /// + /// # Parameters + /// + /// * `enum_name` - The name of the PHP enum to create + /// + /// # Returns + /// + /// A new `EnumEntity` instance configured for the specified enum type + pub fn new(enum_name: impl Into) -> Self { + Self { + enum_name: ensure_end_with_zero(enum_name), + enum_type: B::enum_type(), + method_entities: Vec::new(), + cases: Vec::new(), + constants: Vec::new(), + interfaces: Vec::new(), + _p: PhantomData, + } + } + + /// Add a case to the enum with the given name and value. + /// + /// # Parameters + /// + /// * `name` - The name of the enum case + /// * `value` - The value associated with the enum case, type determined by + /// backing type B + pub fn add_case(&mut self, name: impl Into, value: B) { + let case_name = ensure_end_with_zero(name); + self.cases.push(EnumCase { + name: case_name.clone(), + value: value.into(), + }); + } + + /// Adds a static method to the enum. + /// + /// # Parameters + /// + /// * `name` - The name of the method + /// * `vis` - The visibility of the method (public, protected, or private) + /// * `handler` - The function that implements the method logic + /// + /// # Returns + /// + /// A mutable reference to the created `MethodEntity` for further + /// configuration + pub fn add_static_method( + &mut self, name: impl Into, vis: Visibility, handler: F, + ) -> &mut MethodEntity + where + F: Fn(&mut [ZVal]) -> Result + 'static, + Z: Into + 'static, + E: Throwable + 'static, + { + let mut entity = MethodEntity::new(name, Some(Rc::new(Function::new(handler))), vis); + entity.set_vis_static(); + self.method_entities.push(entity); + self.method_entities.last_mut().unwrap() + } + + /// Adds a constant to the enum. + /// + /// # Parameters + /// + /// * `name` - The name of the constant + /// * `value` - The value of the constant, which will be converted to a + /// Scalar + pub fn add_constant(&mut self, name: impl Into, value: impl Into) { + let constant = ConstantEntity::new(name, value); + self.constants.push(constant); + } + + /// Registers the enum to implement the specified interface. + /// + /// # Parameters + /// + /// * `interface` - The interface that the enum should implement + pub fn implements(&mut self, interface: Interface) { + self.interfaces.push(interface); + } + + unsafe fn function_entries(&self) -> *const zend_function_entry { + unsafe { + let mut methods = self + .method_entities + .iter() + .map(|method| FunctionEntry::from_method_entity(method)) + .collect::>(); + + methods.push(zeroed::()); + + Box::into_raw(methods.into_boxed_slice()).cast() + } + } + + pub(crate) fn handler_map(&self) -> HandlerMap { + self.method_entities + .iter() + .filter_map(|method| { + method.handler.as_ref().map(|handler| { + ( + (Some(self.enum_name.clone()), method.name.clone()), + handler.clone(), + ) + }) + }) + .collect() + } + + #[allow(clippy::useless_conversion)] + pub(crate) unsafe fn init(&self) -> *mut zend_class_entry { + unsafe { + let backing_type = match self.enum_type { + EnumType::Pure => IS_NULL, + EnumType::IntBacked => IS_LONG, + EnumType::StringBacked => IS_STRING, + } as u8; + + let class_ce = zend_register_internal_enum( + self.enum_name.as_ptr().cast(), + backing_type, + self.function_entries(), + ); + + for interface in &self.interfaces { + let interface_ce = interface.as_class_entry().as_ptr(); + zend_class_implements(class_ce, 1, interface_ce); + } + + for constant in &self.constants { + add_class_constant(class_ce, constant); + } + + // Register all enum cases + for case in &self.cases { + register_enum_case(class_ce, &case.name, &case.value); + } + + class_ce + } + } +} + +/// Helper function to register an enum case with the PHP engine. +/// +/// # Parameters +/// +/// * `class_ce` - Pointer to the class entry +/// * `case_name` - Name of the enum case +/// * `case_value` - Value associated with the case +unsafe fn register_enum_case( + class_ce: *mut zend_class_entry, case_name: &CStr, case_value: &Scalar, +) { + unsafe { + match case_value { + Scalar::I64(value) => { + zend_enum_add_case_cstr( + class_ce, + case_name.as_ptr(), + ZVal::from(*value).as_mut_ptr(), + ); + } + Scalar::String(value) => { + #[allow(clippy::useless_conversion)] + let value_ptr = phper_zend_string_init( + value.as_ptr().cast(), + value.len().try_into().unwrap(), + true.into(), + ); + let mut value = MaybeUninit::::uninit(); + phper_zval_str(value.as_mut_ptr(), value_ptr); + + zend_enum_add_case_cstr(class_ce, case_name.as_ptr(), value.as_mut_ptr()); + } + Scalar::Null => { + zend_enum_add_case_cstr(class_ce, case_name.as_ptr(), null_mut()); + } + _ => unreachable!(), + }; + } +} diff --git a/phper/src/lib.rs b/phper/src/lib.rs index fb22512b..1a2800c5 100644 --- a/phper/src/lib.rs +++ b/phper/src/lib.rs @@ -11,6 +11,7 @@ #![warn(rust_2018_idioms, missing_docs)] #![warn(clippy::dbg_macro, clippy::print_stdout)] #![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] #[macro_use] mod macros; @@ -18,6 +19,7 @@ mod macros; pub mod arrays; pub mod classes; pub(crate) mod constants; +pub mod enums; pub mod errors; pub mod functions; pub mod ini; diff --git a/phper/src/modules.rs b/phper/src/modules.rs index f1fba874..469a3a42 100644 --- a/phper/src/modules.rs +++ b/phper/src/modules.rs @@ -61,6 +61,12 @@ unsafe extern "C" fn module_startup(_type: c_int, module_number: c_int) -> c_int module.handler_map.extend(class_entity.handler_map()); } + #[cfg(phper_enum_supported)] + for enum_entity in &module.enum_entities { + enum_entity.init(); + module.handler_map.extend(enum_entity.handler_map()); + } + if let Some(f) = take(&mut module.module_init) { f(); } @@ -140,6 +146,8 @@ pub struct Module { function_entities: Vec, class_entities: Vec>, interface_entities: Vec, + #[cfg(phper_enum_supported)] + enum_entities: Vec>, constants: Vec, ini_entities: Vec, infos: HashMap, @@ -163,6 +171,8 @@ impl Module { function_entities: vec![], class_entities: Default::default(), interface_entities: Default::default(), + #[cfg(phper_enum_supported)] + enum_entities: Default::default(), constants: Default::default(), ini_entities: Default::default(), infos: Default::default(), @@ -219,6 +229,16 @@ impl Module { bound_interface } + /// Register enum to module. + #[cfg(phper_enum_supported)] + pub fn add_enum( + &mut self, enum_entity: crate::enums::EnumEntity, + ) { + self.enum_entities.push(unsafe { + transmute::, crate::enums::EnumEntity<()>>(enum_entity) + }); + } + /// Register constant to module. pub fn add_constant(&mut self, name: impl Into, value: impl Into) { self.constants.push(Constant::new(name, value)); diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index 039e552c..bbc0a29b 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -33,4 +33,5 @@ phper-build = { workspace = true } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(phper_major_version, values("8"))', + 'cfg(phper_enum_supported)', ] } diff --git a/tests/integration/src/enums.rs b/tests/integration/src/enums.rs new file mode 100644 index 00000000..0f6fc3e9 --- /dev/null +++ b/tests/integration/src/enums.rs @@ -0,0 +1,66 @@ +// Copyright (c) 2022 PHPER Framework Team +// PHPER is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +#![cfg(phper_enum_supported)] + +use phper::{classes::Visibility, enums::EnumEntity, modules::Module}; +use std::convert::Infallible; + +pub fn integrate(module: &mut Module) { + // Create pure enum (without backing type) + create_pure_enum(module); + + // Create int-backed enum + create_int_backed_enum(module); + + // Create string-backed enum + create_string_backed_enum(module); +} + +fn create_pure_enum(module: &mut Module) { + let mut enum_entity = EnumEntity::new("IntegrationTest\\PureEnum"); + + // Add enum cases + enum_entity.add_case("ONE", ()); + enum_entity.add_case("TWO", ()); + enum_entity.add_case("THREE", ()); + + // Add constants + enum_entity.add_constant("VERSION", "1.0.0"); + + // Add static method + enum_entity.add_static_method("getDescription", Visibility::Public, |_| { + Ok::<_, Infallible>("Pure enum implementation") + }); + + module.add_enum(enum_entity); +} + +fn create_int_backed_enum(module: &mut Module) { + let mut enum_entity = EnumEntity::::new("IntegrationTest\\IntEnum"); + + // Add enum cases with integer values + enum_entity.add_case("LOW", 1); + enum_entity.add_case("MEDIUM", 5); + enum_entity.add_case("HIGH", 10); + + module.add_enum(enum_entity); +} + +fn create_string_backed_enum(module: &mut Module) { + let mut enum_entity = EnumEntity::::new("IntegrationTest\\StringEnum"); + + // Add enum cases with string values + enum_entity.add_case("RED", "FF0000".to_string()); + enum_entity.add_case("GREEN", "00FF00".to_string()); + enum_entity.add_case("BLUE", "0000FF".to_string()); + + module.add_enum(enum_entity); +} diff --git a/tests/integration/src/lib.rs b/tests/integration/src/lib.rs index 6c9292e0..72a60921 100644 --- a/tests/integration/src/lib.rs +++ b/tests/integration/src/lib.rs @@ -14,6 +14,7 @@ mod arguments; mod arrays; mod classes; mod constants; +mod enums; mod errors; mod functions; mod ini; @@ -45,6 +46,8 @@ pub fn get_module() -> Module { errors::integrate(&mut module); references::integrate(&mut module); typehints::integrate(&mut module); + #[cfg(phper_enum_supported)] + enums::integrate(&mut module); module } diff --git a/tests/integration/tests/integration.rs b/tests/integration/tests/integration.rs index f6c5bbf8..7abe2a66 100644 --- a/tests/integration/tests/integration.rs +++ b/tests/integration/tests/integration.rs @@ -43,6 +43,7 @@ fn test_cli() { &tests_php_dir.join("errors.php"), &tests_php_dir.join("reflection.php"), &tests_php_dir.join("typehints.php"), + &tests_php_dir.join("enums.php"), ], ); } @@ -70,4 +71,5 @@ fn test_fpm() { test_fpm_request("GET", &tests_php_dir, "/values.php", None, None); test_fpm_request("GET", &tests_php_dir, "/constants.php", None, None); test_fpm_request("GET", &tests_php_dir, "/ini.php", None, None); + test_fpm_request("GET", &tests_php_dir, "/enums.php", None, None); } diff --git a/tests/integration/tests/php/enums.php b/tests/integration/tests/php/enums.php new file mode 100644 index 00000000..a45a806c --- /dev/null +++ b/tests/integration/tests/php/enums.php @@ -0,0 +1,53 @@ +name, 'ONE'); +assert_eq((IntegrationTest\PureEnum::TWO)->name, 'TWO'); +assert_eq((IntegrationTest\PureEnum::THREE)->name, 'THREE'); + +// Test int-backed enum +assert_true(enum_exists('IntegrationTest\IntEnum'), 'IntEnum should exist'); +assert_eq((IntegrationTest\IntEnum::LOW)->value, 1, 'IntEnum::LOW value should be 1'); +assert_eq((IntegrationTest\IntEnum::MEDIUM)->value, 5, 'IntEnum::MEDIUM value should be 5'); +assert_eq((IntegrationTest\IntEnum::HIGH)->value, 10, 'IntEnum::HIGH value should be 10'); + +// Test string-backed enum +assert_true(enum_exists('IntegrationTest\StringEnum'), 'StringEnum should exist'); +assert_eq((IntegrationTest\StringEnum::RED)->value, 'FF0000', 'StringEnum::RED value should be FF0000'); +assert_eq((IntegrationTest\StringEnum::GREEN)->value, '00FF00', 'StringEnum::GREEN value should be 00FF00'); +assert_eq((IntegrationTest\StringEnum::BLUE)->value, '0000FF', 'StringEnum::BLUE value should be 0000FF'); + +// Test reflection API +$reflection = new ReflectionEnum(IntegrationTest\StringEnum::class); +assert_true($reflection->isBacked(), 'StringEnum should be a backed enum'); +assert_true($reflection->hasCase('RED'), 'StringEnum should have a RED case'); +assert_true($reflection->hasCase('GREEN'), 'StringEnum should have a GREEN case'); +assert_true($reflection->hasCase('BLUE'), 'StringEnum should have a BLUE case'); diff --git a/tests/integration/tests/php/phpinfo.php b/tests/integration/tests/php/phpinfo.php index d3e1bfdf..44fae0cd 100644 --- a/tests/integration/tests/php/phpinfo.php +++ b/tests/integration/tests/php/phpinfo.php @@ -10,4 +10,8 @@ // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. +ob_start(); phpinfo(); +$output = ob_get_contents(); +ob_end_clean(); +echo substr($output, 0, 100) . '...';