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