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}