Skip to main content

ext_php_rs/
enum_.rs

1//! This module defines the `PhpEnum` trait and related types for Rust enums
2//! that are exported to PHP.
3use std::ptr;
4
5use crate::{
6    boxed::ZBox,
7    class::RegisteredClass,
8    convert::{FromZendObject, FromZval, IntoZendObject, IntoZval},
9    describe::DocComments,
10    error::{Error, Result},
11    ffi::zend_enum_get_case,
12    flags::{ClassFlags, DataType},
13    types::{ZendObject, ZendStr, Zval},
14};
15
16/// Implemented on Rust enums which are exported to PHP.
17pub trait RegisteredEnum {
18    /// The cases of the enum.
19    const CASES: &'static [EnumCase];
20
21    /// # Errors
22    ///
23    /// - [`Error::InvalidProperty`] if the enum does not have a case with the
24    ///   given name, an error is returned.
25    fn from_name(name: &str) -> Result<Self>
26    where
27        Self: Sized;
28
29    /// Returns the variant name of the enum as it is registered in PHP.
30    fn to_name(&self) -> &'static str;
31}
32
33impl<T> FromZendObject<'_> for T
34where
35    T: RegisteredEnum,
36{
37    fn from_zend_object(obj: &ZendObject) -> Result<Self> {
38        if !ClassFlags::from_bits_truncate(unsafe { (*obj.ce).ce_flags }).contains(ClassFlags::Enum)
39        {
40            return Err(Error::ZvalConversion(DataType::ANY_OBJECT));
41        }
42
43        let name = obj
44            .get_properties()?
45            .get("name")
46            .and_then(Zval::indirect)
47            .and_then(Zval::str)
48            .ok_or_else(|| Error::InvalidProperty {
49                property: "name".to_string(),
50            })?;
51
52        T::from_name(name)
53    }
54}
55
56impl<T> FromZval<'_> for T
57where
58    T: RegisteredEnum + RegisteredClass,
59{
60    const TYPE: DataType = DataType::object(T::CLASS_NAME);
61
62    fn from_zval(zval: &Zval) -> Option<Self> {
63        zval.object()
64            .and_then(|obj| Self::from_zend_object(obj).ok())
65    }
66}
67
68impl<T> IntoZendObject for T
69where
70    T: RegisteredEnum + RegisteredClass,
71{
72    fn into_zend_object(self) -> Result<ZBox<ZendObject>> {
73        let case = zend_case(&self);
74        // SAFETY: `zend_case` yields a valid object for the rest of the request.
75        // The class constant table keeps its own reference, so the box takes a
76        // reference of its own before assuming ownership of the pointer.
77        unsafe {
78            (*case).add_ref();
79            Ok(ZBox::from_raw(case))
80        }
81    }
82}
83
84impl<T> IntoZval for T
85where
86    T: RegisteredEnum + RegisteredClass,
87{
88    const TYPE: DataType = DataType::object(T::CLASS_NAME);
89    const NULLABLE: bool = false;
90
91    fn set_zval(self, zv: &mut Zval, _persistent: bool) -> Result<()> {
92        let case = zend_case(&self);
93        // SAFETY: `zend_case` yields a valid object for the rest of the request
94        // and `set_object` takes its own reference, so the class constant table
95        // keeps the one it already holds.
96        zv.set_object(unsafe { &mut *case });
97        Ok(())
98    }
99}
100
101/// Resolves the PHP object backing an enum case.
102///
103/// The object is owned by the class constant table and stays alive until the
104/// end of the request. The pointer is borrowed: whoever stores it must take a
105/// reference of its own with `add_ref`.
106fn zend_case<T: RegisteredEnum + RegisteredClass>(case: &T) -> *mut ZendObject {
107    let mut name = ZendStr::new(T::to_name(case), false);
108    // SAFETY: the class entry is registered and `to_name` only produces names
109    // declared as cases of this enum, which `zend_enum_get_case` requires.
110    unsafe {
111        zend_enum_get_case(
112            ptr::from_ref(T::get_metadata().ce()).cast_mut(),
113            &raw mut *name,
114        )
115    }
116}
117
118/// Represents a case in a PHP enum.
119pub struct EnumCase {
120    /// The identifier of the enum case, e.g. `Bar` in `enum Foo { Bar }`.
121    pub name: &'static str,
122    /// The value of the enum case, which can be an integer or a string.
123    pub discriminant: Option<Discriminant>,
124    /// The documentation comments for the enum case.
125    pub docs: DocComments,
126}
127
128impl EnumCase {
129    /// Gets the PHP data type of the enum case's discriminant.
130    #[must_use]
131    pub fn data_type(&self) -> DataType {
132        match self.discriminant {
133            Some(Discriminant::Int(_)) => DataType::Long,
134            Some(Discriminant::String(_)) => DataType::String,
135            None => DataType::Undef,
136        }
137    }
138}
139
140/// Represents the discriminant of an enum case in PHP, which can be either an
141/// integer or a string.
142#[derive(Debug, PartialEq, Eq)]
143pub enum Discriminant {
144    /// An integer discriminant.
145    Int(i64),
146    /// A string discriminant.
147    String(&'static str),
148}
149
150impl TryFrom<&Discriminant> for Zval {
151    type Error = Error;
152
153    fn try_from(value: &Discriminant) -> Result<Self> {
154        match value {
155            Discriminant::Int(i) => i.into_zval(false),
156            Discriminant::String(s) => s.into_zval(false),
157        }
158    }
159}
160
161#[cfg(test)]
162#[cfg(feature = "embed")]
163mod tests {
164    #![allow(clippy::unwrap_used)]
165    use super::*;
166    use crate::embed::Embed;
167
168    #[test]
169    fn test_zval_try_from_discriminant() {
170        Embed::run(|| {
171            let zval_int: Zval = Zval::try_from(&Discriminant::Int(42)).unwrap();
172            assert!(zval_int.is_long());
173            assert_eq!(zval_int.long().unwrap(), 42);
174
175            let zval_str: Zval = Zval::try_from(&Discriminant::String("foo")).unwrap();
176            assert!(zval_str.is_string());
177            assert_eq!(zval_str.string().unwrap().clone(), "foo");
178        });
179    }
180}