Skip to main content

phper/
enums.rs

1// Copyright (c) 2022 PHPER Framework Team
2// PHPER is licensed under Mulan PSL v2.
3// You can use this software according to the terms and conditions of the Mulan
4// PSL v2. You may obtain a copy of Mulan PSL v2 at:
5//          http://license.coscl.org.cn/MulanPSL2
6// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
7// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
8// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
9// See the Mulan PSL v2 for more details.
10
11//! APIs related to PHP enum functionality.
12//!
13//! This module provides Rust wrappers for PHP enum functionality, allowing you
14//! to define and work with PHP enums from Rust code. It supports pure enums,
15//! integer-backed enums, and string-backed enums, corresponding to their PHP
16//! counterparts.
17//!
18//! The implementation respects the PHP 8.1+ enum feature set and provides a
19//! type-safe interface for creating enum cases and handling enum values.
20#![cfg(all(phper_major_version = "8", not(phper_minor_version = "0")))]
21
22use crate::{
23    classes::{
24        ClassEntry, ConstantEntity, InnerClassEntry, Interface, Visibility, add_class_constant,
25    },
26    errors::Throwable,
27    functions::{Function, FunctionEntry, HandlerMap, MethodEntity},
28    objects::ZObj,
29    strings::ZString,
30    sys::*,
31    types::Scalar,
32    utils::ensure_end_with_zero,
33    values::ZVal,
34};
35use sealed::sealed;
36use std::{
37    cell::RefCell,
38    ffi::{CStr, CString},
39    marker::PhantomData,
40    mem::{ManuallyDrop, zeroed},
41    ptr::{null, null_mut},
42    rc::Rc,
43};
44
45/// Trait representing a backing type for enum values.
46///
47/// This trait is implemented by types that can serve as backing values
48/// for PHP enums. The trait is sealed to ensure only supported types
49/// can be used as enum backing types.
50#[sealed]
51pub trait EnumBackingType: Into<Scalar> {
52    /// Returns the PHP enum type representation for this backing type.
53    fn enum_type() -> EnumType;
54}
55
56#[sealed]
57impl EnumBackingType for () {
58    fn enum_type() -> EnumType {
59        EnumType::Pure
60    }
61}
62
63#[sealed]
64impl EnumBackingType for i64 {
65    fn enum_type() -> EnumType {
66        EnumType::IntBacked
67    }
68}
69
70#[sealed]
71impl EnumBackingType for String {
72    fn enum_type() -> EnumType {
73        EnumType::StringBacked
74    }
75}
76
77/// Enum type in PHP.
78///
79/// Represents the three possible types of PHP enums:
80/// - Pure enums (no backing value)
81/// - Integer-backed enums
82/// - String-backed enums
83pub enum EnumType {
84    /// Pure enum (like `enum Foo { case A, case B }`)
85    Pure,
86    /// Int backed enum (like `enum Foo: int { case A = 1, case B = 2 }`)
87    IntBacked,
88    /// String backed enum (like `enum Foo: string { case A = 'a', case B = 'b'
89    /// }`)
90    StringBacked,
91}
92
93/// Enum case definition for PHP enum.
94///
95/// Represents a single case within a PHP enum, storing its name
96/// and associated value.
97struct EnumCaseEntity {
98    name: CString,
99    value: Scalar,
100}
101
102/// Represents an enum case within a PHP enum.
103///
104/// `EnumCase` provides a convenient way to access a specific enum case
105/// without repeatedly calling `Enum::get_case()` with the case name.
106/// It stores a reference to the enum and the name of the case.
107#[derive(Clone)]
108pub struct EnumCase {
109    r#enum: Enum,
110    case_name: String,
111}
112
113impl EnumCase {
114    /// Creates a new `EnumCase` with the specified enum and case name.
115    ///
116    /// # Parameters
117    ///
118    /// * `enum_obj` - The enum containing the case
119    /// * `case_name` - The name of the enum case
120    fn new(enum_obj: Enum, case_name: impl Into<String>) -> Self {
121        Self {
122            r#enum: enum_obj,
123            case_name: case_name.into(),
124        }
125    }
126
127    /// Gets a reference to the enum case.
128    ///
129    /// # Returns
130    ///
131    /// A reference to ZObj representing the enum case, or an error if the case
132    /// doesn't exist
133    pub fn get_case<'a>(&self) -> &'a ZObj {
134        unsafe { self.r#enum.get_case(&self.case_name).unwrap() }
135    }
136
137    /// Gets a mutable reference to the enum case.
138    ///
139    /// # Returns
140    ///
141    /// A mutable reference to ZObj representing the enum case, or an error if
142    /// the case doesn't exist
143    pub fn get_mut_case<'a>(&mut self) -> &'a mut ZObj {
144        unsafe { self.r#enum.get_mut_case(&self.case_name).unwrap() }
145    }
146
147    /// Gets the name of the enum case.
148    pub fn name(&self) -> &str {
149        &self.case_name
150    }
151
152    /// Gets the enum this case belongs to.
153    pub fn as_enum(&self) -> &Enum {
154        &self.r#enum
155    }
156}
157
158/// The [Enum] holds [zend_class_entry] for PHP enum, created by
159/// [Module::add_enum](crate::modules::Module::add_enum) or
160/// [EnumEntity::bound_enum].
161///
162/// When the enum registered (module initialized), the [Enum] will
163/// be initialized, so you can use the [Enum] to get enum cases, etc.
164///
165/// # Examples
166///
167/// ```rust
168/// use phper::{
169///     enums::{Enum, EnumEntity},
170///     modules::Module,
171///     php_get_module,
172/// };
173///
174/// fn make_status_enum() -> EnumEntity {
175///     let mut enum_entity = EnumEntity::new("Status");
176///     enum_entity.add_case("Active", ());
177///     enum_entity.add_case("Inactive", ());
178///     enum_entity.add_case("Pending", ());
179///     enum_entity
180/// }
181///
182/// #[php_get_module]
183/// pub fn get_module() -> Module {
184///     let mut module = Module::new(
185///         env!("CARGO_CRATE_NAME"),
186///         env!("CARGO_PKG_VERSION"),
187///         env!("CARGO_PKG_AUTHORS"),
188///     );
189///
190///     let _status_enum: Enum = module.add_enum(make_status_enum());
191///
192///     module
193/// }
194/// ```
195#[derive(Clone)]
196pub struct Enum {
197    inner: Rc<RefCell<InnerClassEntry>>,
198}
199
200impl Enum {
201    /// Creates a null Enum reference. Used internally.
202    fn null() -> Self {
203        Self {
204            inner: Rc::new(RefCell::new(InnerClassEntry::Ptr(null()))),
205        }
206    }
207
208    /// Create from name, which will be looked up from globals.
209    pub fn from_name(name: impl Into<String>) -> Self {
210        Self {
211            inner: Rc::new(RefCell::new(InnerClassEntry::Name(name.into()))),
212        }
213    }
214
215    fn bind(&self, ptr: *mut zend_class_entry) {
216        match &mut *self.inner.borrow_mut() {
217            InnerClassEntry::Ptr(p) => {
218                *p = ptr;
219            }
220            InnerClassEntry::Name(_) => {
221                unreachable!("Cannot bind() an Enum created with from_name()");
222            }
223        }
224    }
225
226    /// Converts to class entry.
227    pub fn as_class_entry(&self) -> &ClassEntry {
228        let inner = self.inner.borrow().clone();
229        match inner {
230            InnerClassEntry::Ptr(ptr) => unsafe { ClassEntry::from_ptr(ptr) },
231            InnerClassEntry::Name(name) => {
232                let entry = ClassEntry::from_globals(name).unwrap();
233                *self.inner.borrow_mut() = InnerClassEntry::Ptr(entry.as_ptr());
234                entry
235            }
236        }
237    }
238
239    /// Get an enum case by name.
240    ///
241    /// # Parameters
242    ///
243    /// * `case_name` - The name of the enum case to retrieve
244    ///
245    /// # Returns
246    ///
247    /// A reference to ZObj representing the enum case, or an error if the case
248    /// doesn't exist
249    ///
250    /// # Safety
251    ///
252    /// This function is marked as unsafe because the underlying
253    /// `zend_enum_get_case` function may cause a SIGSEGV (segmentation
254    /// fault) when the case doesn't exist. Even though this method attempts
255    /// to check for null and return an error instead, there might still be
256    /// scenarios where the PHP internal function behaves unpredictably.
257    /// Callers must ensure the enum and case name are valid before calling this
258    /// function.
259    pub unsafe fn get_case<'a>(&self, case_name: impl AsRef<str>) -> crate::Result<&'a ZObj> {
260        unsafe {
261            let ce = self.as_class_entry().as_ptr() as *mut _;
262            let case_name_str = case_name.as_ref();
263            let mut name_zstr = ZString::new(case_name_str);
264
265            // Get the enum case
266            let case_obj = zend_enum_get_case(ce, name_zstr.as_mut_ptr());
267
268            // Convert to &ZObj
269            Ok(ZObj::from_ptr(case_obj))
270        }
271    }
272
273    /// Get a mutable reference to an enum case by name.
274    ///
275    /// # Parameters
276    ///
277    /// * `case_name` - The name of the enum case to retrieve
278    ///
279    /// # Returns
280    ///
281    /// A mutable reference to ZObj representing the enum case, or an error if
282    /// the case doesn't exist
283    ///
284    /// # Safety
285    ///
286    /// This function is marked as unsafe because the underlying
287    /// `zend_enum_get_case` function may cause a SIGSEGV (segmentation
288    /// fault) when the case doesn't exist. Even though this method attempts
289    /// to check for null and return an error instead, there might still be
290    /// scenarios where the PHP internal function behaves unpredictably.
291    /// Callers must ensure the enum and case name are valid before calling this
292    /// function.
293    pub unsafe fn get_mut_case<'a>(
294        &mut self, case_name: impl AsRef<str>,
295    ) -> crate::Result<&'a mut ZObj> {
296        unsafe {
297            let ce = self.as_class_entry().as_ptr() as *mut _;
298            let case_name_str = case_name.as_ref();
299            let mut name_zstr = ZString::new(case_name_str);
300
301            // Get the enum case
302            let case_obj = zend_enum_get_case(ce, name_zstr.as_mut_ptr());
303
304            // Convert to &mut ZObj
305            Ok(ZObj::from_mut_ptr(case_obj as *mut _))
306        }
307    }
308}
309
310/// Builder for registering a PHP enum.
311///
312/// This struct facilitates the creation and registration of PHP enums from Rust
313/// code. The generic parameter B represents the backing type and determines the
314/// enum type.
315///
316/// # Type Parameters
317///
318/// * `B` - A type that implements `EnumBackingType`, determining the enum's
319///   backing type. Use `()` for pure enums, `i64` for int-backed enums, or
320///   `String` for string-backed enums.
321pub struct EnumEntity<B: EnumBackingType = ()> {
322    enum_name: CString,
323    enum_type: EnumType,
324    method_entities: Vec<MethodEntity>,
325    cases: Vec<EnumCaseEntity>,
326    constants: Vec<ConstantEntity>,
327    interfaces: Vec<Interface>,
328    bound_enum: Enum,
329    _p: PhantomData<(B, *mut ())>,
330}
331
332impl<B: EnumBackingType> EnumEntity<B> {
333    /// Creates a new enum builder with the specified name.
334    ///
335    /// # Parameters
336    ///
337    /// * `enum_name` - The name of the PHP enum to create
338    ///
339    /// # Returns
340    ///
341    /// A new `EnumEntity` instance configured for the specified enum type
342    pub fn new(enum_name: impl Into<String>) -> Self {
343        Self {
344            enum_name: ensure_end_with_zero(enum_name),
345            enum_type: B::enum_type(),
346            method_entities: Vec::new(),
347            cases: Vec::new(),
348            constants: Vec::new(),
349            interfaces: Vec::new(),
350            bound_enum: Enum::null(),
351            _p: PhantomData,
352        }
353    }
354
355    /// Add a case to the enum with the given name and value.
356    ///
357    /// # Parameters
358    ///
359    /// * `name` - The name of the enum case
360    /// * `value` - The value associated with the enum case, type determined by
361    ///   backing type B
362    ///
363    /// # Returns
364    ///
365    /// An `EnumCase` instance representing the added case
366    pub fn add_case(&mut self, name: impl Into<String>, value: B) -> EnumCase {
367        let case_name_str = name.into();
368        let case_name = ensure_end_with_zero(&case_name_str);
369        self.cases.push(EnumCaseEntity {
370            name: case_name,
371            value: value.into(),
372        });
373        EnumCase::new(self.bound_enum(), case_name_str)
374    }
375
376    /// Adds a static method to the enum.
377    ///
378    /// # Parameters
379    ///
380    /// * `name` - The name of the method
381    /// * `vis` - The visibility of the method (public, protected, or private)
382    /// * `handler` - The function that implements the method logic
383    ///
384    /// # Returns
385    ///
386    /// A mutable reference to the created `MethodEntity` for further
387    /// configuration
388    pub fn add_static_method<F, Z, E>(
389        &mut self, name: impl Into<String>, vis: Visibility, handler: F,
390    ) -> &mut MethodEntity
391    where
392        F: Fn(&mut [ZVal]) -> Result<Z, E> + 'static,
393        Z: Into<ZVal> + 'static,
394        E: Throwable + 'static,
395    {
396        let mut entity = MethodEntity::new(name, Some(Rc::new(Function::new(handler))), vis);
397        entity.set_vis_static();
398        self.method_entities.push(entity);
399        self.method_entities.last_mut().unwrap()
400    }
401
402    /// Adds a constant to the enum.
403    ///
404    /// # Parameters
405    ///
406    /// * `name` - The name of the constant
407    /// * `value` - The value of the constant, which will be converted to a
408    ///   Scalar
409    pub fn add_constant(&mut self, name: impl Into<String>, value: impl Into<Scalar>) {
410        let constant = ConstantEntity::new(name, value);
411        self.constants.push(constant);
412    }
413
414    /// Registers the enum to implement the specified interface.
415    ///
416    /// # Parameters
417    ///
418    /// * `interface` - The interface that the enum should implement
419    pub fn implements(&mut self, interface: Interface) {
420        self.interfaces.push(interface);
421    }
422
423    /// Get the bound enum.
424    ///
425    /// # Examples
426    ///
427    /// ```
428    /// use phper::{alloc::ToRefOwned, classes::Visibility, enums::EnumEntity};
429    ///
430    /// pub fn make_status_enum() -> EnumEntity {
431    ///     let mut enum_entity = EnumEntity::new("Status");
432    ///     enum_entity.add_case("Active", ());
433    ///     enum_entity.add_case("Inactive", ());
434    ///     let mut status_enum = enum_entity.bound_enum();
435    ///     enum_entity.add_static_method("getActiveCase", Visibility::Public, move |_| {
436    ///         let active_case = unsafe { status_enum.clone().get_mut_case("Active")? };
437    ///         phper::ok(active_case.to_ref_owned())
438    ///     });
439    ///     enum_entity
440    /// }
441    /// ```
442    #[inline]
443    pub fn bound_enum(&self) -> Enum {
444        self.bound_enum.clone()
445    }
446
447    unsafe fn function_entries(&self) -> *const zend_function_entry {
448        unsafe {
449            let mut methods = self
450                .method_entities
451                .iter()
452                .map(|method| FunctionEntry::from_method_entity(method))
453                .collect::<Vec<_>>();
454
455            methods.push(zeroed::<zend_function_entry>());
456
457            Box::into_raw(methods.into_boxed_slice()).cast()
458        }
459    }
460
461    pub(crate) fn handler_map(&self) -> HandlerMap {
462        self.method_entities
463            .iter()
464            .filter_map(|method| {
465                method.handler.as_ref().map(|handler| {
466                    (
467                        (Some(self.enum_name.clone()), method.name.clone()),
468                        handler.clone(),
469                    )
470                })
471            })
472            .collect()
473    }
474
475    #[allow(clippy::useless_conversion)]
476    pub(crate) unsafe fn init(&self) -> *mut zend_class_entry {
477        unsafe {
478            let backing_type = match self.enum_type {
479                EnumType::Pure => IS_NULL,
480                EnumType::IntBacked => IS_LONG,
481                EnumType::StringBacked => IS_STRING,
482            } as u8;
483
484            let class_ce = zend_register_internal_enum(
485                self.enum_name.as_ptr().cast(),
486                backing_type,
487                self.function_entries(),
488            );
489
490            self.bound_enum.bind(class_ce);
491
492            for interface in &self.interfaces {
493                let interface_ce = interface.as_class_entry().as_ptr();
494                zend_class_implements(class_ce, 1, interface_ce);
495            }
496
497            for constant in &self.constants {
498                add_class_constant(class_ce, constant);
499            }
500
501            // Register all enum cases
502            for case in &self.cases {
503                register_enum_case(class_ce, &case.name, &case.value);
504            }
505
506            class_ce
507        }
508    }
509}
510
511/// Helper function to register an enum case with the PHP engine.
512///
513/// # Parameters
514///
515/// * `class_ce` - Pointer to the class entry
516/// * `case_name` - Name of the enum case
517/// * `case_value` - Value associated with the case
518unsafe fn register_enum_case(
519    class_ce: *mut zend_class_entry, case_name: &CStr, case_value: &Scalar,
520) {
521    unsafe {
522        match case_value {
523            Scalar::I64(value) => {
524                zend_enum_add_case_cstr(
525                    class_ce,
526                    case_name.as_ptr(),
527                    ZVal::from(*value).as_mut_ptr(),
528                );
529            }
530            Scalar::String(value) => {
531                let value = ZString::new_persistent(value);
532                let mut value = ManuallyDrop::new(ZVal::from(value));
533                zend_enum_add_case_cstr(class_ce, case_name.as_ptr(), value.as_mut_ptr());
534            }
535            Scalar::Null => {
536                zend_enum_add_case_cstr(class_ce, case_name.as_ptr(), null_mut());
537            }
538            _ => unreachable!(),
539        };
540    }
541}