Skip to main content

ext_php_rs/builders/
class.rs

1use std::{
2    ffi::CString,
3    mem::{ManuallyDrop, MaybeUninit},
4    ptr,
5    rc::Rc,
6};
7
8use crate::{
9    args::ArgInfoTables,
10    builders::{FunctionBuilder, function::free_registered_entries},
11    class::{ClassEntryInfo, ConstructorMeta, ConstructorResult, RegisteredClass},
12    convert::{IntoZval, IntoZvalDyn},
13    describe::DocComments,
14    error::{Error, Result},
15    exception::PhpException,
16    ffi::{
17        zend_declare_class_constant_ex, zend_declare_property, zend_do_implement_interface,
18        zend_hash_str_find_ptr_lc, zend_register_internal_class_ex,
19        zend_register_internal_interface,
20    },
21    flags::{ClassFlags, ConstantFlags, DataType, MethodFlags, PropertyFlags},
22    types::{ZendClassObject, ZendObject, ZendStr, Zval},
23    zend::{ClassEntry, CompilerGlobals, ExecuteData, ExecutorGlobals, FunctionEntry},
24    zend_fastcall,
25};
26
27/// A constant entry: (name, `value_closure`, docs, `stub_value`, flags)
28type ConstantEntry = (
29    String,
30    Box<dyn FnOnce() -> Result<Zval>>,
31    DocComments,
32    String,
33    ConstantFlags,
34);
35type PropertyDefault = Option<Box<dyn FnOnce() -> Result<Zval>>>;
36
37/// Metadata for a class property to be registered with PHP.
38pub struct ClassProperty {
39    /// Name of the property.
40    pub name: String,
41    /// Visibility and modifier flags.
42    pub flags: PropertyFlags,
43    /// Optional default value closure.
44    pub default: PropertyDefault,
45    /// Documentation comments.
46    pub docs: DocComments,
47    /// PHP type for stub generation.
48    pub ty: Option<DataType>,
49    /// Whether the property accepts null.
50    pub nullable: bool,
51    /// Whether the property is read-only (getter without setter).
52    pub readonly: bool,
53    /// PHP stub representation of the default value (e.g. `"null"`, `"42"`).
54    pub default_stub: Option<String>,
55}
56
57/// Returns the first name that repeats an earlier one. The engine keys
58/// method, function and class tables by ASCII-lowercased name, and constant and
59/// property tables by the exact name.
60pub(crate) fn first_duplicate<'a>(
61    names: impl IntoIterator<Item = &'a str>,
62    fold_ascii_case: bool,
63) -> Option<&'a str> {
64    let names: Vec<&str> = names.into_iter().collect();
65    names.iter().enumerate().find_map(|(i, name)| {
66        names[..i]
67            .iter()
68            .any(|earlier| {
69                if fold_ascii_case {
70                    earlier.eq_ignore_ascii_case(name)
71                } else {
72                    earlier == name
73                }
74            })
75            .then_some(*name)
76    })
77}
78
79/// Returns an error when the engine already holds a class, interface or enum
80/// named `name`, ignoring ASCII case. `do_register_internal_class` would
81/// silently replace it.
82///
83/// `EG(class_table)` is only set at request startup, so this reads
84/// `CG(class_table)`, which holds every class registered so far.
85pub(crate) fn ensure_class_name_free(name: &str) -> Result<()> {
86    let class_table = CompilerGlobals::get().class_table;
87    if class_table.is_null() {
88        return Ok(());
89    }
90    // SAFETY: `class_table` is the engine's live class table and `name` is
91    // valid for `name.len()` bytes; the lookup copies it before lowercasing.
92    let found = unsafe { zend_hash_str_find_ptr_lc(class_table, name.as_ptr().cast(), name.len()) };
93    if found.is_null() {
94        Ok(())
95    } else {
96        Err(Error::DuplicateClass {
97            class: name.to_owned(),
98        })
99    }
100}
101
102/// Builder for registering a class in PHP.
103#[must_use]
104pub struct ClassBuilder {
105    pub(crate) name: String,
106    ce: ClassEntry,
107    pub(crate) extends: Option<ClassEntryInfo>,
108    pub(crate) interfaces: Vec<ClassEntryInfo>,
109    pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>,
110    object_override: Option<unsafe extern "C" fn(class_type: *mut ClassEntry) -> *mut ZendObject>,
111    pub(crate) properties: Vec<ClassProperty>,
112    pub(crate) constants: Vec<ConstantEntry>,
113    register: Option<fn(&'static mut ClassEntry, ArgInfoTables)>,
114    pub(crate) docs: DocComments,
115}
116
117impl ClassBuilder {
118    /// Creates a new class builder, used to build classes
119    /// to be exported to PHP.
120    ///
121    /// # Parameters
122    ///
123    /// * `name` - The name of the class.
124    pub fn new<T: Into<String>>(name: T) -> Self {
125        Self {
126            name: name.into(),
127            // SAFETY: A zeroed class entry is in an initialized state, as it is a raw C type
128            // whose fields do not have a drop implementation.
129            ce: unsafe { MaybeUninit::zeroed().assume_init() },
130            extends: None,
131            interfaces: vec![],
132            methods: vec![],
133            object_override: None,
134            properties: vec![],
135            constants: vec![],
136            register: None,
137            docs: &[],
138        }
139    }
140
141    /// Return PHP class flags
142    #[must_use]
143    pub fn get_flags(&self) -> u32 {
144        self.ce.ce_flags
145    }
146
147    /// Sets the class builder to extend another class.
148    ///
149    /// # Parameters
150    ///
151    /// * `parent` - The parent class to extend.
152    pub fn extends(mut self, parent: ClassEntryInfo) -> Self {
153        self.extends = Some(parent);
154        self
155    }
156
157    /// Implements an interface on the class.
158    ///
159    /// # Parameters
160    ///
161    /// * `interface` - Interface to implement on the class.
162    ///
163    /// # Panics
164    ///
165    /// Panics when the given class entry `interface` is not an interface.
166    pub fn implements(mut self, interface: ClassEntryInfo) -> Self {
167        self.interfaces.push(interface);
168        self
169    }
170
171    /// Adds a method to the class.
172    ///
173    /// # Parameters
174    ///
175    /// * `func` - The function builder to add to the class.
176    /// * `flags` - Flags relating to the function. See [`MethodFlags`].
177    pub fn method(mut self, func: FunctionBuilder<'static>, flags: MethodFlags) -> Self {
178        self.methods.push((func, flags));
179        self
180    }
181
182    /// Adds a property to the class.
183    ///
184    /// # Parameters
185    ///
186    /// * `prop` - The property metadata to add to the class.
187    pub fn property(mut self, prop: ClassProperty) -> Self {
188        self.properties.push(prop);
189        self
190    }
191
192    /// Adds a constant to the class. The type of the constant is defined by the
193    /// type of the given default.
194    ///
195    /// Returns a result containing the class builder if the constant was
196    /// successfully added.
197    ///
198    /// # Parameters
199    ///
200    /// * `name` - The name of the constant to add to the class.
201    /// * `value` - The value of the constant.
202    /// * `docs` - Documentation comments for the constant.
203    /// * `flags` - Visibility of the constant.
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if `value` cannot be converted into a [`Zval`].
208    pub fn constant<T: Into<String>>(
209        mut self,
210        name: T,
211        value: impl IntoZval + 'static,
212        docs: DocComments,
213        flags: ConstantFlags,
214    ) -> Result<Self> {
215        let zval = value.into_zval(true)?;
216        let stub = crate::convert::zval_to_stub(&zval);
217        self.constants
218            .push((name.into(), Box::new(|| Ok(zval)), docs, stub, flags));
219        Ok(self)
220    }
221
222    /// Adds a constant to the class from a `dyn` object. The type of the
223    /// constant is defined by the type of the value.
224    ///
225    /// Returns a result containing the class builder if the constant was
226    /// successfully added.
227    ///
228    /// # Parameters
229    ///
230    /// * `name` - The name of the constant to add to the class.
231    /// * `value` - The value of the constant.
232    /// * `docs` - Documentation comments for the constant.
233    /// * `flags` - Visibility of the constant.
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if `value` cannot be converted into a [`Zval`].
238    pub fn dyn_constant<T: Into<String>>(
239        mut self,
240        name: T,
241        value: &'static dyn IntoZvalDyn,
242        docs: DocComments,
243        flags: ConstantFlags,
244    ) -> Result<Self> {
245        let stub = value.stub_value();
246        let value = Rc::new(value);
247        self.constants.push((
248            name.into(),
249            Box::new(move || value.as_zval(true)),
250            docs,
251            stub,
252            flags,
253        ));
254        Ok(self)
255    }
256
257    /// Sets the flags for the class.
258    ///
259    /// # Parameters
260    ///
261    /// * `flags` - Flags relating to the class. See [`ClassFlags`].
262    pub fn flags(mut self, flags: ClassFlags) -> Self {
263        self.ce.ce_flags = flags.bits();
264        self
265    }
266
267    /// Overrides the creation of the Zend object which will represent an
268    /// instance of this class.
269    ///
270    /// # Parameters
271    ///
272    /// * `T` - The type which will override the Zend object. Must implement
273    ///   [`RegisteredClass`] which can be derived using the
274    ///   [`php_class`](crate::php_class) attribute macro.
275    ///
276    /// # Panics
277    ///
278    /// Panics if the class name associated with `T` is not the same as the
279    /// class name specified when creating the builder.
280    pub fn object_override<T: RegisteredClass>(mut self) -> Self {
281        extern "C" fn create_object<T: RegisteredClass>(ce: *mut ClassEntry) -> *mut ZendObject {
282            // Try to initialize with a default instance if available.
283            // This is critical for exception classes that extend \Exception, because
284            // PHP's zend_throw_exception_ex creates objects via create_object without
285            // calling the constructor, then immediately accesses properties.
286            // Without default initialization, accessing properties on uninitialized
287            // objects would panic.
288            if let Some(instance) = T::default_init() {
289                let obj = ZendClassObject::<T>::new(instance).into_raw();
290                // SAFETY: `into_raw` yields a valid, exclusively owned object that
291                // the engine takes over.
292                return unsafe { (*obj).get_mut_zend_obj() };
293            }
294
295            // SAFETY: After calling this function, PHP will always call the constructor
296            // defined below, which assumes that the object is uninitialized.
297            let obj = unsafe { ZendClassObject::<T>::new_uninit(ce.as_ref()) }.into_raw();
298            // SAFETY: same ownership transfer as above.
299            unsafe { (*obj).get_mut_zend_obj() }
300        }
301
302        zend_fastcall! {
303            extern fn constructor<T: RegisteredClass>(ex: &mut ExecuteData, _: &mut Zval) {
304                crate::zend::run_handler(std::panic::AssertUnwindSafe(|| {
305                    let Some(ConstructorMeta { constructor, .. }) = T::constructor() else {
306                        PhpException::from_message("You cannot instantiate this class from PHP.".into())
307                            .throw();
308                        return;
309                    };
310
311                    let this = match constructor(ex) {
312                        ConstructorResult::Ok(this) => this,
313                        ConstructorResult::Exception(e) => {
314                            e.throw();
315                            return;
316                        }
317                        ConstructorResult::ArgError => return,
318                    };
319
320                    // Use get_object_uninit because the Rust backing is not yet initialized.
321                    // We need access to the ZendClassObject to call initialize() on it.
322                    let Some(this_obj) = ex.get_object_uninit::<T>() else {
323                        PhpException::from_message("Failed to retrieve reference to `this` object.".into())
324                            .throw();
325                        return;
326                    };
327
328                    this_obj.initialize(this);
329                }));
330            }
331        }
332
333        debug_assert_eq!(
334            self.name.as_str(),
335            T::CLASS_NAME,
336            "Class name in builder does not match class name in `impl RegisteredClass`."
337        );
338        self.object_override = Some(create_object::<T>);
339        let is_interface = T::FLAGS.contains(ClassFlags::Interface);
340
341        // For interfaces: only add __construct if explicitly declared
342        // For classes: always add __construct (PHP needs it for object creation)
343        if let Some(ConstructorMeta {
344            build_fn, flags, ..
345        }) = T::constructor()
346        {
347            let func = if is_interface {
348                FunctionBuilder::new_abstract("__construct")
349            } else {
350                FunctionBuilder::new("__construct", constructor::<T>)
351            };
352            let visibility = flags.unwrap_or(MethodFlags::Public);
353            self.method(build_fn(func), visibility)
354        } else if is_interface {
355            // Don't add default constructor for interfaces
356            self
357        } else {
358            // Add default constructor for classes
359            let func = FunctionBuilder::new("__construct", constructor::<T>);
360            self.method(func, MethodFlags::Public)
361        }
362    }
363
364    /// Function to register the class with PHP, called once the class is built.
365    ///
366    /// It receives the registered class entry and the argument info tables of
367    /// the class's methods. `zend_register_functions` borrows those tables for
368    /// the life of the process, so the function must park them somewhere that
369    /// lives that long — normally the class's own
370    /// [`ClassMetadata`](crate::class::ClassMetadata) static:
371    ///
372    /// ```rust,ignore
373    /// .registration(|ce, arg_info| {
374    ///     META.set_ce(ce);
375    ///     META.set_arg_info(arg_info);
376    /// })
377    /// ```
378    ///
379    /// # Parameters
380    ///
381    /// * `register` - The function to call to register the class.
382    pub fn registration(mut self, register: fn(&'static mut ClassEntry, ArgInfoTables)) -> Self {
383        self.register = Some(register);
384        self
385    }
386
387    /// Sets the documentation for the class.
388    ///
389    /// # Parameters
390    ///
391    /// * `docs` - The documentation comments for the class.
392    pub fn docs(mut self, docs: DocComments) -> Self {
393        self.docs = docs;
394        self
395    }
396
397    /// Checks that no two properties, methods or constants of the class share
398    /// a PHP name. Methods are compared ignoring ASCII case, like the engine.
399    fn validate(&self) -> Result<()> {
400        if let Some(name) = first_duplicate(self.properties.iter().map(|p| p.name.as_str()), false)
401        {
402            return Err(Error::DuplicateProperty {
403                property: format!("{}::${name}", self.name),
404            });
405        }
406        if let Some(name) = first_duplicate(self.methods.iter().map(|(m, _)| m.name.as_str()), true)
407        {
408            return Err(Error::DuplicateMethod {
409                method: format!("{}::{name}", self.name),
410            });
411        }
412        if let Some(name) = first_duplicate(self.constants.iter().map(|c| c.0.as_str()), false) {
413            return Err(Error::DuplicateConstant {
414                constant: format!("{}::{name}", self.name),
415            });
416        }
417        Ok(())
418    }
419
420    /// Builds and registers the class.
421    ///
422    /// # Errors
423    ///
424    /// * [`Error::DuplicateProperty`], [`Error::DuplicateMethod`] or
425    ///   [`Error::DuplicateConstant`] - If two members share a PHP name.
426    /// * [`Error::DuplicateClass`] - If a class with this name, ignoring ASCII
427    ///   case, is already registered.
428    /// * [`Error::InvalidPointer`] - If the class could not be registered.
429    /// * [`Error::InvalidCString`] - If the class name is not a valid C string.
430    /// * [`Error::IntegerOverflow`] - If the property flags are not valid.
431    /// * If a method or property could not be built.
432    ///
433    /// # Panics
434    ///
435    /// * If called outside a module startup (MINIT) function.
436    /// * If no registration function was provided.
437    pub fn register(mut self) -> Result<()> {
438        assert!(
439            !ExecutorGlobals::get().current_module.is_null(),
440            "Classes can only be registered from a module startup (MINIT) function: \
441             `do_register_internal_class` dereferences `EG(current_module)`."
442        );
443        self.validate()?;
444        ensure_class_name_free(&self.name)?;
445
446        self.ce.name = ZendStr::new_interned(&self.name, true).into_raw();
447
448        let mut arg_info = Vec::with_capacity(self.methods.len());
449        let mut methods = Vec::with_capacity(self.methods.len() + 1);
450        for (method, flags) in self.methods {
451            let (mut entry, args) = method.build()?;
452            entry.flags |= flags.bits();
453            methods.push(entry);
454            arg_info.push(args);
455        }
456
457        // The engine keeps `zend_internal_function.arg_info` pointing into these
458        // for the life of the process, so `register` parks them. Holding them in
459        // a `ManuallyDrop` keeps them alive across registration, which reads
460        // them, without a second owner. Every fallible step below runs after the
461        // class entry is in `CG(class_table)`, so an error must leak the tables
462        // rather than dangle the functions the engine already holds.
463        let arg_info = ManuallyDrop::new(arg_info.into_boxed_slice());
464
465        methods.push(FunctionEntry::end());
466        let entries = Box::into_raw(methods.into_boxed_slice());
467        self.ce.info.internal.builtin_functions = entries.cast::<FunctionEntry>().cast_const();
468
469        let class = if self.ce.flags().contains(ClassFlags::Interface) {
470            unsafe { zend_register_internal_interface(&raw mut self.ce) }
471        } else {
472            // Resolved here and not before the branch: the getter is usually
473            // `T::get_metadata().ce()`, which panics when the parent has not been
474            // registered, and `zend_register_internal_interface` takes no parent.
475            let parent = match self.extends {
476                Some((ptr, _)) => ptr::from_ref(ptr()).cast_mut(),
477                None => std::ptr::null_mut(),
478            };
479            unsafe { zend_register_internal_class_ex(&raw mut self.ce, parent) }
480        };
481
482        // SAFETY: `do_register_internal_class` has interned every `fname` and read
483        // `builtin_functions` for the last time, so the table is dead.
484        unsafe { free_registered_entries(entries) };
485
486        let class = unsafe { class.as_mut() }.ok_or(Error::InvalidPointer)?;
487        class.info.internal.builtin_functions = ptr::null();
488
489        // disable serialization if the class has an associated object
490        if self.object_override.is_some() {
491            cfg_if::cfg_if! {
492                if #[cfg(php81)] {
493                    class.ce_flags |= ClassFlags::NotSerializable.bits();
494                } else {
495                    class.serialize = Some(crate::ffi::zend_class_serialize_deny);
496                    class.unserialize = Some(crate::ffi::zend_class_unserialize_deny);
497                }
498            }
499        }
500
501        for (iface, _) in self.interfaces {
502            let interface = iface();
503            assert!(
504                interface.is_interface(),
505                "Given class entry was not an interface."
506            );
507
508            unsafe { zend_do_implement_interface(class, ptr::from_ref(interface).cast_mut()) };
509        }
510
511        for prop in self.properties {
512            let mut default_zval = match prop.default {
513                Some(f) => f()?,
514                None => Zval::new(),
515            };
516            unsafe {
517                zend_declare_property(
518                    class,
519                    CString::new(prop.name.as_str())?.as_ptr(),
520                    prop.name.len() as _,
521                    &raw mut default_zval,
522                    prop.flags.bits().try_into()?,
523                );
524            }
525        }
526
527        for (name, value, _, _, flags) in self.constants {
528            // The constants table keeps the interned key; releasing an interned
529            // string is a no-op, so dropping the `ZBox` afterwards is sound.
530            let name = ZendStr::new_interned(&name, true);
531            // `zend_declare_class_constant_ex` takes the payload with
532            // `ZVAL_COPY_VALUE` and no incref and, unlike
533            // `zend_declare_typed_property`, does not reject refcounted values, so
534            // the constants table owns it and the `Zval` must not run its destructor.
535            let mut value = ManuallyDrop::new(value()?);
536            unsafe {
537                zend_declare_class_constant_ex(
538                    class,
539                    name.as_ptr().cast_mut(),
540                    &raw mut *value,
541                    flags.bits().try_into()?,
542                    ptr::null_mut(),
543                );
544            };
545        }
546
547        if let Some(object_override) = self.object_override {
548            class.__bindgen_anon_2.create_object = Some(object_override);
549        }
550
551        if let Some(register) = self.register {
552            register(class, ManuallyDrop::into_inner(arg_info));
553        } else {
554            panic!("Class {} was not registered.", self.name);
555        }
556
557        Ok(())
558    }
559}
560
561#[cfg(test)]
562mod tests {
563    use crate::test::test_function;
564
565    use super::*;
566
567    #[test]
568    #[allow(unpredictable_function_pointer_comparisons)]
569    fn test_new() {
570        let class = ClassBuilder::new("Foo");
571        assert_eq!(class.name, "Foo");
572        assert_eq!(class.extends, None);
573        assert_eq!(class.interfaces, vec![]);
574        assert_eq!(class.methods.len(), 0);
575        assert_eq!(class.object_override, None);
576        assert!(class.properties.is_empty());
577        assert_eq!(class.constants.len(), 0);
578        assert_eq!(class.register, None);
579        assert_eq!(class.docs, &[] as DocComments);
580    }
581
582    #[test]
583    fn test_extends() {
584        let extends: ClassEntryInfo = (|| todo!(), "Bar");
585        let class = ClassBuilder::new("Foo").extends(extends);
586        assert_eq!(class.extends, Some(extends));
587    }
588
589    #[test]
590    fn test_implements() {
591        let implements: ClassEntryInfo = (|| todo!(), "Bar");
592        let class = ClassBuilder::new("Foo").implements(implements);
593        assert_eq!(class.interfaces, vec![implements]);
594    }
595
596    #[test]
597    fn test_method() {
598        let method = FunctionBuilder::new("foo", test_function);
599        let class = ClassBuilder::new("Foo").method(method, MethodFlags::Public);
600        assert_eq!(class.methods.len(), 1);
601    }
602
603    #[test]
604    fn test_property() {
605        let class = ClassBuilder::new("Foo").property(ClassProperty {
606            name: "bar".into(),
607            flags: PropertyFlags::Public,
608            default: None,
609            docs: &["Doc 1"],
610            ty: Some(DataType::String),
611            nullable: false,
612            readonly: false,
613            default_stub: None,
614        });
615        assert_eq!(class.properties.len(), 1);
616        assert_eq!(class.properties[0].name, "bar");
617        assert_eq!(class.properties[0].flags, PropertyFlags::Public);
618        assert!(class.properties[0].default.is_none());
619        assert_eq!(class.properties[0].docs, &["Doc 1"] as DocComments);
620        assert_eq!(class.properties[0].ty, Some(DataType::String));
621    }
622
623    fn named_property(name: &str) -> ClassProperty {
624        ClassProperty {
625            name: name.into(),
626            flags: PropertyFlags::Public,
627            default: None,
628            docs: &[],
629            ty: None,
630            nullable: false,
631            readonly: false,
632            default_stub: None,
633        }
634    }
635
636    fn with_constant(mut class: ClassBuilder, name: &str) -> ClassBuilder {
637        class.constants.push((
638            name.into(),
639            Box::new(|| Err(Error::InvalidPointer)),
640            &[],
641            String::new(),
642            ConstantFlags::Public,
643        ));
644        class
645    }
646
647    #[test]
648    fn first_duplicate_folds_ascii_case_only_when_asked() {
649        assert_eq!(first_duplicate(["a", "b", "a"], false), Some("a"));
650        assert_eq!(first_duplicate(["a", "B", "b"], false), None);
651        assert_eq!(first_duplicate(["a", "B", "b"], true), Some("b"));
652        assert_eq!(first_duplicate(["ä", "Ä"], true), None);
653        assert_eq!(first_duplicate([], true), None);
654    }
655
656    #[test]
657    fn validate_rejects_a_repeated_property() {
658        let mut class = ClassBuilder::new("Foo");
659        class.properties = vec![named_property("a"), named_property("a")];
660        assert!(matches!(
661            class.validate(),
662            Err(Error::DuplicateProperty { property }) if property == "Foo::$a"
663        ));
664    }
665
666    #[test]
667    fn validate_rejects_methods_that_differ_only_by_case() {
668        let class = ClassBuilder::new("Foo")
669            .method(
670                FunctionBuilder::new("run", test_function),
671                MethodFlags::Public,
672            )
673            .method(
674                FunctionBuilder::new("RUN", test_function),
675                MethodFlags::Static,
676            );
677        assert!(matches!(
678            class.validate(),
679            Err(Error::DuplicateMethod { method }) if method == "Foo::RUN"
680        ));
681    }
682
683    #[test]
684    fn validate_compares_constants_exactly() {
685        let class = with_constant(with_constant(ClassBuilder::new("Foo"), "A"), "a");
686        assert!(class.validate().is_ok());
687        let class = with_constant(class, "A");
688        assert!(matches!(
689            class.validate(),
690            Err(Error::DuplicateConstant { constant }) if constant == "Foo::A"
691        ));
692    }
693
694    #[test]
695    #[cfg(feature = "embed")]
696    fn class_names_already_in_the_class_table_are_taken() {
697        let ran = crate::embed::Embed::run(|| {
698            assert!(matches!(
699                ensure_class_name_free("STDCLASS"),
700                Err(Error::DuplicateClass { class }) if class == "STDCLASS"
701            ));
702            assert!(ensure_class_name_free("ExtPhpRsNoSuchClass").is_ok());
703            true
704        });
705        assert!(ran);
706    }
707
708    #[test]
709    fn validate_accepts_distinct_members() {
710        let mut class = ClassBuilder::new("Foo")
711            .method(
712                FunctionBuilder::new("run", test_function),
713                MethodFlags::Public,
714            )
715            .method(
716                FunctionBuilder::new("stop", test_function),
717                MethodFlags::Public,
718            );
719        class.properties = vec![named_property("a"), named_property("b")];
720        assert!(with_constant(class, "A").validate().is_ok());
721    }
722
723    #[test]
724    #[cfg(feature = "embed")]
725    fn test_constant() {
726        let class = ClassBuilder::new("Foo")
727            .constant("bar", 42, &["Doc 1"], ConstantFlags::Public)
728            .expect("Failed to create constant");
729        assert_eq!(class.constants.len(), 1);
730        assert_eq!(class.constants[0].0, "bar");
731        assert_eq!(class.constants[0].2, &["Doc 1"] as DocComments);
732    }
733
734    #[test]
735    #[cfg(feature = "embed")]
736    fn test_dyn_constant() {
737        let class = ClassBuilder::new("Foo")
738            .dyn_constant("bar", &42, &["Doc 1"], ConstantFlags::Public)
739            .expect("Failed to create constant");
740        assert_eq!(class.constants.len(), 1);
741        assert_eq!(class.constants[0].0, "bar");
742        assert_eq!(class.constants[0].2, &["Doc 1"] as DocComments);
743    }
744
745    #[test]
746    fn test_flags() {
747        let class = ClassBuilder::new("Foo").flags(ClassFlags::Abstract);
748        assert_eq!(class.ce.ce_flags, ClassFlags::Abstract.bits());
749    }
750
751    #[test]
752    fn test_registration() {
753        let class = ClassBuilder::new("Foo").registration(|_, _| {});
754        assert!(class.register.is_some());
755    }
756
757    #[test]
758    fn test_registration_interface() {
759        let class = ClassBuilder::new("Foo")
760            .flags(ClassFlags::Interface)
761            .registration(|_, _| {});
762        assert!(class.register.is_some());
763    }
764
765    #[test]
766    fn test_docs() {
767        let class = ClassBuilder::new("Foo").docs(&["Doc 1"]);
768        assert_eq!(class.docs, &["Doc 1"] as DocComments);
769    }
770
771    // TODO: Test the register function
772}