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