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