Skip to main content

ext_php_rs/builders/
class.rs

1use std::{ffi::CString, mem::MaybeUninit, ptr, rc::Rc};
2
3use crate::{
4    builders::FunctionBuilder,
5    class::{ClassEntryInfo, ConstructorMeta, ConstructorResult, RegisteredClass},
6    convert::{IntoZval, IntoZvalDyn},
7    describe::DocComments,
8    error::{Error, Result},
9    exception::PhpException,
10    ffi::{
11        zend_declare_class_constant, zend_declare_property, zend_do_implement_interface,
12        zend_register_internal_class_ex, zend_register_internal_interface,
13    },
14    flags::{ClassFlags, DataType, MethodFlags, PropertyFlags},
15    types::{ZendClassObject, ZendObject, ZendStr, Zval},
16    zend::{ClassEntry, ExecuteData, FunctionEntry},
17    zend_fastcall,
18};
19
20/// A constant entry: (name, `value_closure`, docs, `stub_value`)
21type ConstantEntry = (
22    String,
23    Box<dyn FnOnce() -> Result<Zval>>,
24    DocComments,
25    String,
26);
27type PropertyDefault = Option<Box<dyn FnOnce() -> Result<Zval>>>;
28
29/// Metadata for a class property to be registered with PHP.
30pub struct ClassProperty {
31    /// Name of the property.
32    pub name: String,
33    /// Visibility and modifier flags.
34    pub flags: PropertyFlags,
35    /// Optional default value closure.
36    pub default: PropertyDefault,
37    /// Documentation comments.
38    pub docs: DocComments,
39    /// PHP type for stub generation.
40    pub ty: Option<DataType>,
41    /// Whether the property accepts null.
42    pub nullable: bool,
43    /// Whether the property is read-only (getter without setter).
44    pub readonly: bool,
45    /// PHP stub representation of the default value (e.g. `"null"`, `"42"`).
46    pub default_stub: Option<String>,
47}
48
49/// Builder for registering a class in PHP.
50#[must_use]
51pub struct ClassBuilder {
52    pub(crate) name: String,
53    ce: ClassEntry,
54    pub(crate) extends: Option<ClassEntryInfo>,
55    pub(crate) interfaces: Vec<ClassEntryInfo>,
56    pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>,
57    object_override: Option<unsafe extern "C" fn(class_type: *mut ClassEntry) -> *mut ZendObject>,
58    pub(crate) properties: Vec<ClassProperty>,
59    pub(crate) constants: Vec<ConstantEntry>,
60    register: Option<fn(&'static mut ClassEntry)>,
61    pub(crate) docs: DocComments,
62}
63
64impl ClassBuilder {
65    /// Creates a new class builder, used to build classes
66    /// to be exported to PHP.
67    ///
68    /// # Parameters
69    ///
70    /// * `name` - The name of the class.
71    pub fn new<T: Into<String>>(name: T) -> Self {
72        Self {
73            name: name.into(),
74            // SAFETY: A zeroed class entry is in an initialized state, as it is a raw C type
75            // whose fields do not have a drop implementation.
76            ce: unsafe { MaybeUninit::zeroed().assume_init() },
77            extends: None,
78            interfaces: vec![],
79            methods: vec![],
80            object_override: None,
81            properties: vec![],
82            constants: vec![],
83            register: None,
84            docs: &[],
85        }
86    }
87
88    /// Return PHP class flags
89    #[must_use]
90    pub fn get_flags(&self) -> u32 {
91        self.ce.ce_flags
92    }
93
94    /// Sets the class builder to extend another class.
95    ///
96    /// # Parameters
97    ///
98    /// * `parent` - The parent class to extend.
99    pub fn extends(mut self, parent: ClassEntryInfo) -> Self {
100        self.extends = Some(parent);
101        self
102    }
103
104    /// Implements an interface on the class.
105    ///
106    /// # Parameters
107    ///
108    /// * `interface` - Interface to implement on the class.
109    ///
110    /// # Panics
111    ///
112    /// Panics when the given class entry `interface` is not an interface.
113    pub fn implements(mut self, interface: ClassEntryInfo) -> Self {
114        self.interfaces.push(interface);
115        self
116    }
117
118    /// Adds a method to the class.
119    ///
120    /// # Parameters
121    ///
122    /// * `func` - The function builder to add to the class.
123    /// * `flags` - Flags relating to the function. See [`MethodFlags`].
124    pub fn method(mut self, func: FunctionBuilder<'static>, flags: MethodFlags) -> Self {
125        self.methods.push((func, flags));
126        self
127    }
128
129    /// Adds a property to the class.
130    ///
131    /// # Parameters
132    ///
133    /// * `prop` - The property metadata to add to the class.
134    pub fn property(mut self, prop: ClassProperty) -> Self {
135        self.properties.push(prop);
136        self
137    }
138
139    /// Adds a constant to the class. The type of the constant is defined by the
140    /// type of the given default.
141    ///
142    /// Returns a result containing the class builder if the constant was
143    /// successfully added.
144    ///
145    /// # Parameters
146    ///
147    /// * `name` - The name of the constant to add to the class.
148    /// * `value` - The value of the constant.
149    /// * `docs` - Documentation comments for the constant.
150    ///
151    /// # Errors
152    ///
153    /// TODO: Never?
154    pub fn constant<T: Into<String>>(
155        mut self,
156        name: T,
157        value: impl IntoZval + 'static,
158        docs: DocComments,
159    ) -> Result<Self> {
160        // Convert to Zval first to get stub value
161        let zval = value.into_zval(true)?;
162        let stub = crate::convert::zval_to_stub(&zval);
163        self.constants
164            .push((name.into(), Box::new(|| Ok(zval)), docs, stub));
165        Ok(self)
166    }
167
168    /// Adds a constant to the class from a `dyn` object. The type of the
169    /// constant is defined by the type of the value.
170    ///
171    /// Returns a result containing the class builder if the constant was
172    /// successfully added.
173    ///
174    /// # Parameters
175    ///
176    /// * `name` - The name of the constant to add to the class.
177    /// * `value` - The value of the constant.
178    /// * `docs` - Documentation comments for the constant.
179    ///
180    /// # Errors
181    ///
182    /// TODO: Never?
183    pub fn dyn_constant<T: Into<String>>(
184        mut self,
185        name: T,
186        value: &'static dyn IntoZvalDyn,
187        docs: DocComments,
188    ) -> Result<Self> {
189        let stub = value.stub_value();
190        let value = Rc::new(value);
191        self.constants.push((
192            name.into(),
193            Box::new(move || value.as_zval(true)),
194            docs,
195            stub,
196        ));
197        Ok(self)
198    }
199
200    /// Sets the flags for the class.
201    ///
202    /// # Parameters
203    ///
204    /// * `flags` - Flags relating to the class. See [`ClassFlags`].
205    pub fn flags(mut self, flags: ClassFlags) -> Self {
206        self.ce.ce_flags = flags.bits();
207        self
208    }
209
210    /// Overrides the creation of the Zend object which will represent an
211    /// instance of this class.
212    ///
213    /// # Parameters
214    ///
215    /// * `T` - The type which will override the Zend object. Must implement
216    ///   [`RegisteredClass`] which can be derived using the
217    ///   [`php_class`](crate::php_class) attribute macro.
218    ///
219    /// # Panics
220    ///
221    /// Panics if the class name associated with `T` is not the same as the
222    /// class name specified when creating the builder.
223    pub fn object_override<T: RegisteredClass>(mut self) -> Self {
224        extern "C" fn create_object<T: RegisteredClass>(ce: *mut ClassEntry) -> *mut ZendObject {
225            // Try to initialize with a default instance if available.
226            // This is critical for exception classes that extend \Exception, because
227            // PHP's zend_throw_exception_ex creates objects via create_object without
228            // calling the constructor, then immediately accesses properties.
229            // Without default initialization, accessing properties on uninitialized
230            // objects would panic.
231            if let Some(instance) = T::default_init() {
232                let obj = ZendClassObject::<T>::new(instance);
233                return obj.into_raw().get_mut_zend_obj();
234            }
235
236            // SAFETY: After calling this function, PHP will always call the constructor
237            // defined below, which assumes that the object is uninitialized.
238            let obj = unsafe { ZendClassObject::<T>::new_uninit(ce.as_ref()) };
239            obj.into_raw().get_mut_zend_obj()
240        }
241
242        zend_fastcall! {
243            extern fn constructor<T: RegisteredClass>(ex: &mut ExecuteData, _: &mut Zval) {
244                use crate::zend::try_catch;
245                use std::panic::AssertUnwindSafe;
246
247                // Wrap the constructor body with try_catch to ensure Rust destructors
248                // are called if a bailout occurs (issue #537)
249                let catch_result = try_catch(AssertUnwindSafe(|| {
250                    let Some(ConstructorMeta { constructor, .. }) = T::constructor() else {
251                        PhpException::default("You cannot instantiate this class from PHP.".into())
252                            .throw()
253                            .expect("Failed to throw exception when constructing class");
254                        return;
255                    };
256
257                    let this = match constructor(ex) {
258                        ConstructorResult::Ok(this) => this,
259                        ConstructorResult::Exception(e) => {
260                            e.throw()
261                                .expect("Failed to throw exception while constructing class");
262                            return;
263                        }
264                        ConstructorResult::ArgError => return,
265                    };
266
267                    // Use get_object_uninit because the Rust backing is not yet initialized.
268                    // We need access to the ZendClassObject to call initialize() on it.
269                    let Some(this_obj) = ex.get_object_uninit::<T>() else {
270                        PhpException::default("Failed to retrieve reference to `this` object.".into())
271                            .throw()
272                            .expect("Failed to throw exception while constructing class");
273                        return;
274                    };
275
276                    this_obj.initialize(this);
277                }));
278
279                // If there was a bailout, re-trigger it after Rust cleanup
280                if catch_result.is_err() {
281                    unsafe { crate::zend::bailout(); }
282                }
283            }
284        }
285
286        debug_assert_eq!(
287            self.name.as_str(),
288            T::CLASS_NAME,
289            "Class name in builder does not match class name in `impl RegisteredClass`."
290        );
291        self.object_override = Some(create_object::<T>);
292        let is_interface = T::FLAGS.contains(ClassFlags::Interface);
293
294        // For interfaces: only add __construct if explicitly declared
295        // For classes: always add __construct (PHP needs it for object creation)
296        if let Some(ConstructorMeta {
297            build_fn, flags, ..
298        }) = T::constructor()
299        {
300            let func = if is_interface {
301                FunctionBuilder::new_abstract("__construct")
302            } else {
303                FunctionBuilder::new("__construct", constructor::<T>)
304            };
305            let visibility = flags.unwrap_or(MethodFlags::Public);
306            self.method(build_fn(func), visibility)
307        } else if is_interface {
308            // Don't add default constructor for interfaces
309            self
310        } else {
311            // Add default constructor for classes
312            let func = FunctionBuilder::new("__construct", constructor::<T>);
313            self.method(func, MethodFlags::Public)
314        }
315    }
316
317    /// Function to register the class with PHP. This function is called after
318    /// the class is built.
319    ///
320    /// # Parameters
321    ///
322    /// * `register` - The function to call to register the class.
323    pub fn registration(mut self, register: fn(&'static mut ClassEntry)) -> Self {
324        self.register = Some(register);
325        self
326    }
327
328    /// Sets the documentation for the class.
329    ///
330    /// # Parameters
331    ///
332    /// * `docs` - The documentation comments for the class.
333    pub fn docs(mut self, docs: DocComments) -> Self {
334        self.docs = docs;
335        self
336    }
337
338    /// Builds and registers the class.
339    ///
340    /// # Errors
341    ///
342    /// * [`Error::InvalidPointer`] - If the class could not be registered.
343    /// * [`Error::InvalidCString`] - If the class name is not a valid C string.
344    /// * [`Error::IntegerOverflow`] - If the property flags are not valid.
345    /// * If a method or property could not be built.
346    ///
347    /// # Panics
348    ///
349    /// If no registration function was provided.
350    pub fn register(mut self) -> Result<()> {
351        self.ce.name = ZendStr::new_interned(&self.name, true).into_raw();
352
353        let mut methods = self
354            .methods
355            .into_iter()
356            .map(|(method, flags)| {
357                method.build().map(|mut method| {
358                    method.flags |= flags.bits();
359                    method
360                })
361            })
362            .collect::<Result<Vec<_>>>()?;
363
364        methods.push(FunctionEntry::end());
365        let func = Box::into_raw(methods.into_boxed_slice()) as *const FunctionEntry;
366        self.ce.info.internal.builtin_functions = func;
367
368        let class = if self.ce.flags().contains(ClassFlags::Interface) {
369            unsafe {
370                zend_register_internal_interface(&raw mut self.ce)
371                    .as_mut()
372                    .ok_or(Error::InvalidPointer)?
373            }
374        } else {
375            unsafe {
376                zend_register_internal_class_ex(
377                    &raw mut self.ce,
378                    match self.extends {
379                        Some((ptr, _)) => ptr::from_ref(ptr()).cast_mut(),
380                        None => std::ptr::null_mut(),
381                    },
382                )
383                .as_mut()
384                .ok_or(Error::InvalidPointer)?
385            }
386        };
387
388        // disable serialization if the class has an associated object
389        if self.object_override.is_some() {
390            cfg_if::cfg_if! {
391                if #[cfg(php81)] {
392                    class.ce_flags |= ClassFlags::NotSerializable.bits();
393                } else {
394                    class.serialize = Some(crate::ffi::zend_class_serialize_deny);
395                    class.unserialize = Some(crate::ffi::zend_class_unserialize_deny);
396                }
397            }
398        }
399
400        for (iface, _) in self.interfaces {
401            let interface = iface();
402            assert!(
403                interface.is_interface(),
404                "Given class entry was not an interface."
405            );
406
407            unsafe { zend_do_implement_interface(class, ptr::from_ref(interface).cast_mut()) };
408        }
409
410        for prop in self.properties {
411            let mut default_zval = match prop.default {
412                Some(f) => f()?,
413                None => Zval::new(),
414            };
415            unsafe {
416                zend_declare_property(
417                    class,
418                    CString::new(prop.name.as_str())?.as_ptr(),
419                    prop.name.len() as _,
420                    &raw mut default_zval,
421                    prop.flags.bits().try_into()?,
422                );
423            }
424        }
425
426        for (name, value, _, _) in self.constants {
427            let value = Box::into_raw(Box::new(value()?));
428            unsafe {
429                zend_declare_class_constant(
430                    class,
431                    CString::new(name.as_str())?.as_ptr(),
432                    name.len(),
433                    value,
434                );
435            };
436        }
437
438        if let Some(object_override) = self.object_override {
439            class.__bindgen_anon_2.create_object = Some(object_override);
440        }
441
442        if let Some(register) = self.register {
443            register(class);
444        } else {
445            panic!("Class {} was not registered.", self.name);
446        }
447
448        Ok(())
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use crate::test::test_function;
455
456    use super::*;
457
458    #[test]
459    #[allow(unpredictable_function_pointer_comparisons)]
460    fn test_new() {
461        let class = ClassBuilder::new("Foo");
462        assert_eq!(class.name, "Foo");
463        assert_eq!(class.extends, None);
464        assert_eq!(class.interfaces, vec![]);
465        assert_eq!(class.methods.len(), 0);
466        assert_eq!(class.object_override, None);
467        assert!(class.properties.is_empty());
468        assert_eq!(class.constants.len(), 0);
469        assert_eq!(class.register, None);
470        assert_eq!(class.docs, &[] as DocComments);
471    }
472
473    #[test]
474    fn test_extends() {
475        let extends: ClassEntryInfo = (|| todo!(), "Bar");
476        let class = ClassBuilder::new("Foo").extends(extends);
477        assert_eq!(class.extends, Some(extends));
478    }
479
480    #[test]
481    fn test_implements() {
482        let implements: ClassEntryInfo = (|| todo!(), "Bar");
483        let class = ClassBuilder::new("Foo").implements(implements);
484        assert_eq!(class.interfaces, vec![implements]);
485    }
486
487    #[test]
488    fn test_method() {
489        let method = FunctionBuilder::new("foo", test_function);
490        let class = ClassBuilder::new("Foo").method(method, MethodFlags::Public);
491        assert_eq!(class.methods.len(), 1);
492    }
493
494    #[test]
495    fn test_property() {
496        let class = ClassBuilder::new("Foo").property(ClassProperty {
497            name: "bar".into(),
498            flags: PropertyFlags::Public,
499            default: None,
500            docs: &["Doc 1"],
501            ty: Some(DataType::String),
502            nullable: false,
503            readonly: false,
504            default_stub: None,
505        });
506        assert_eq!(class.properties.len(), 1);
507        assert_eq!(class.properties[0].name, "bar");
508        assert_eq!(class.properties[0].flags, PropertyFlags::Public);
509        assert!(class.properties[0].default.is_none());
510        assert_eq!(class.properties[0].docs, &["Doc 1"] as DocComments);
511        assert_eq!(class.properties[0].ty, Some(DataType::String));
512    }
513
514    #[test]
515    #[cfg(feature = "embed")]
516    fn test_constant() {
517        let class = ClassBuilder::new("Foo")
518            .constant("bar", 42, &["Doc 1"])
519            .expect("Failed to create constant");
520        assert_eq!(class.constants.len(), 1);
521        assert_eq!(class.constants[0].0, "bar");
522        assert_eq!(class.constants[0].2, &["Doc 1"] as DocComments);
523    }
524
525    #[test]
526    #[cfg(feature = "embed")]
527    fn test_dyn_constant() {
528        let class = ClassBuilder::new("Foo")
529            .dyn_constant("bar", &42, &["Doc 1"])
530            .expect("Failed to create constant");
531        assert_eq!(class.constants.len(), 1);
532        assert_eq!(class.constants[0].0, "bar");
533        assert_eq!(class.constants[0].2, &["Doc 1"] as DocComments);
534    }
535
536    #[test]
537    fn test_flags() {
538        let class = ClassBuilder::new("Foo").flags(ClassFlags::Abstract);
539        assert_eq!(class.ce.ce_flags, ClassFlags::Abstract.bits());
540    }
541
542    #[test]
543    fn test_registration() {
544        let class = ClassBuilder::new("Foo").registration(|_| {});
545        assert!(class.register.is_some());
546    }
547
548    #[test]
549    fn test_registration_interface() {
550        let class = ClassBuilder::new("Foo")
551            .flags(ClassFlags::Interface)
552            .registration(|_| {});
553        assert!(class.register.is_some());
554    }
555
556    #[test]
557    fn test_docs() {
558        let class = ClassBuilder::new("Foo").docs(&["Doc 1"]);
559        assert_eq!(class.docs, &["Doc 1"] as DocComments);
560    }
561
562    // TODO: Test the register function
563}