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
242        let (func, visibility) = if let Some(ConstructorMeta {
243            build_fn, flags, ..
244        }) = T::constructor()
245        {
246            let func = FunctionBuilder::new("__construct", constructor::<T>);
247            (build_fn(func), flags.unwrap_or(MethodFlags::Public))
248        } else {
249            (
250                FunctionBuilder::new("__construct", constructor::<T>),
251                MethodFlags::Public,
252            )
253        };
254
255        self.method(func, visibility)
256    }
257
258    /// Function to register the class with PHP. This function is called after
259    /// the class is built.
260    ///
261    /// # Parameters
262    ///
263    /// * `register` - The function to call to register the class.
264    pub fn registration(mut self, register: fn(&'static mut ClassEntry)) -> Self {
265        self.register = Some(register);
266        self
267    }
268
269    /// Sets the documentation for the class.
270    ///
271    /// # Parameters
272    ///
273    /// * `docs` - The documentation comments for the class.
274    pub fn docs(mut self, docs: DocComments) -> Self {
275        self.docs = docs;
276        self
277    }
278
279    /// Builds and registers the class.
280    ///
281    /// # Errors
282    ///
283    /// * [`Error::InvalidPointer`] - If the class could not be registered.
284    /// * [`Error::InvalidCString`] - If the class name is not a valid C string.
285    /// * [`Error::IntegerOverflow`] - If the property flags are not valid.
286    /// * If a method or property could not be built.
287    ///
288    /// # Panics
289    ///
290    /// If no registration function was provided.
291    pub fn register(mut self) -> Result<()> {
292        self.ce.name = ZendStr::new_interned(&self.name, true).into_raw();
293
294        let mut methods = self
295            .methods
296            .into_iter()
297            .map(|(method, flags)| {
298                method.build().map(|mut method| {
299                    method.flags |= flags.bits();
300                    method
301                })
302            })
303            .collect::<Result<Vec<_>>>()?;
304
305        methods.push(FunctionEntry::end());
306        let func = Box::into_raw(methods.into_boxed_slice()) as *const FunctionEntry;
307        self.ce.info.internal.builtin_functions = func;
308
309        let class = unsafe {
310            zend_register_internal_class_ex(
311                &raw mut self.ce,
312                match self.extends {
313                    Some((ptr, _)) => ptr::from_ref(ptr()).cast_mut(),
314                    None => std::ptr::null_mut(),
315                },
316            )
317            .as_mut()
318            .ok_or(Error::InvalidPointer)?
319        };
320
321        // disable serialization if the class has an associated object
322        if self.object_override.is_some() {
323            cfg_if::cfg_if! {
324                if #[cfg(php81)] {
325                    class.ce_flags |= ClassFlags::NotSerializable.bits();
326                } else {
327                    class.serialize = Some(crate::ffi::zend_class_serialize_deny);
328                    class.unserialize = Some(crate::ffi::zend_class_unserialize_deny);
329                }
330            }
331        }
332
333        for (iface, _) in self.interfaces {
334            let interface = iface();
335            assert!(
336                interface.is_interface(),
337                "Given class entry was not an interface."
338            );
339
340            unsafe { zend_do_implement_interface(class, ptr::from_ref(interface).cast_mut()) };
341        }
342
343        for (name, flags, _) in self.properties {
344            unsafe {
345                zend_declare_property(
346                    class,
347                    CString::new(name.as_str())?.as_ptr(),
348                    name.len() as _,
349                    &mut Zval::new(),
350                    flags.bits().try_into()?,
351                );
352            }
353        }
354
355        for (name, value, _) in self.constants {
356            let value = Box::into_raw(Box::new(value()?));
357            unsafe {
358                zend_declare_class_constant(
359                    class,
360                    CString::new(name.as_str())?.as_ptr(),
361                    name.len(),
362                    value,
363                );
364            };
365        }
366
367        if let Some(object_override) = self.object_override {
368            class.__bindgen_anon_2.create_object = Some(object_override);
369        }
370
371        if let Some(register) = self.register {
372            register(class);
373        } else {
374            panic!("Class {} was not registered.", self.name);
375        }
376
377        Ok(())
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use crate::test::test_function;
384
385    use super::*;
386
387    #[test]
388    #[allow(unpredictable_function_pointer_comparisons)]
389    fn test_new() {
390        let class = ClassBuilder::new("Foo");
391        assert_eq!(class.name, "Foo");
392        assert_eq!(class.extends, None);
393        assert_eq!(class.interfaces, vec![]);
394        assert_eq!(class.methods.len(), 0);
395        assert_eq!(class.object_override, None);
396        assert_eq!(class.properties, vec![]);
397        assert_eq!(class.constants.len(), 0);
398        assert_eq!(class.register, None);
399        assert_eq!(class.docs, &[] as DocComments);
400    }
401
402    #[test]
403    fn test_extends() {
404        let extends: ClassEntryInfo = (|| todo!(), "Bar");
405        let class = ClassBuilder::new("Foo").extends(extends);
406        assert_eq!(class.extends, Some(extends));
407    }
408
409    #[test]
410    fn test_implements() {
411        let implements: ClassEntryInfo = (|| todo!(), "Bar");
412        let class = ClassBuilder::new("Foo").implements(implements);
413        assert_eq!(class.interfaces, vec![implements]);
414    }
415
416    #[test]
417    fn test_method() {
418        let method = FunctionBuilder::new("foo", test_function);
419        let class = ClassBuilder::new("Foo").method(method, MethodFlags::Public);
420        assert_eq!(class.methods.len(), 1);
421    }
422
423    #[test]
424    fn test_property() {
425        let class = ClassBuilder::new("Foo").property("bar", PropertyFlags::Public, &["Doc 1"]);
426        assert_eq!(
427            class.properties,
428            vec![(
429                "bar".to_string(),
430                PropertyFlags::Public,
431                &["Doc 1"] as DocComments
432            )]
433        );
434    }
435
436    #[test]
437    #[cfg(feature = "embed")]
438    fn test_constant() {
439        let class = ClassBuilder::new("Foo")
440            .constant("bar", 42, &["Doc 1"])
441            .expect("Failed to create constant");
442        assert_eq!(class.constants.len(), 1);
443        assert_eq!(class.constants[0].0, "bar");
444        assert_eq!(class.constants[0].2, &["Doc 1"] as DocComments);
445    }
446
447    #[test]
448    #[cfg(feature = "embed")]
449    fn test_dyn_constant() {
450        let class = ClassBuilder::new("Foo")
451            .dyn_constant("bar", &42, &["Doc 1"])
452            .expect("Failed to create constant");
453        assert_eq!(class.constants.len(), 1);
454        assert_eq!(class.constants[0].0, "bar");
455        assert_eq!(class.constants[0].2, &["Doc 1"] as DocComments);
456    }
457
458    #[test]
459    fn test_flags() {
460        let class = ClassBuilder::new("Foo").flags(ClassFlags::Abstract);
461        assert_eq!(class.ce.ce_flags, ClassFlags::Abstract.bits());
462    }
463
464    #[test]
465    fn test_registration() {
466        let class = ClassBuilder::new("Foo").registration(|_| {});
467        assert!(class.register.is_some());
468    }
469
470    #[test]
471    fn test_docs() {
472        let class = ClassBuilder::new("Foo").docs(&["Doc 1"]);
473        assert_eq!(class.docs, &["Doc 1"] as DocComments);
474    }
475
476    // TODO: Test the register function
477}