• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

davidcole1340 / ext-php-rs / 18244744117

04 Oct 2025 01:08PM UTC coverage: 30.759% (+3.0%) from 27.728%
18244744117

Pull #533

github

web-flow
Merge 0936605ac into 113ef33a0
Pull Request #533: Feat/interface impl

68 of 152 new or added lines in 9 files covered. (44.74%)

3 existing lines in 2 files now uncovered.

1326 of 4311 relevant lines covered (30.76%)

7.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.73
/src/builders/class.rs
1
use std::{ffi::CString, mem::MaybeUninit, ptr, rc::Rc};
2

3
use 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
type ConstantEntry = (String, Box<dyn FnOnce() -> Result<Zval>>, DocComments);
21

22
/// Builder for registering a class in PHP.
23
#[must_use]
24
pub 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

37
impl 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 {
13✔
45
        Self {
46
            name: name.into(),
39✔
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() },
39✔
50
            extends: None,
51
            interfaces: vec![],
26✔
52
            methods: vec![],
26✔
53
            object_override: None,
54
            properties: vec![],
26✔
55
            constants: vec![],
26✔
56
            register: None,
57
            docs: &[],
13✔
58
        }
59
    }
60

61
    /// Return PHP class flags
62
    #[must_use]
63
    pub fn get_flags(&self) -> u32 {
1✔
64
        self.ce.ce_flags
1✔
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 {
2✔
73
        self.extends = Some(parent);
2✔
74
        self
2✔
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 {
3✔
87
        self.interfaces.push(interface);
9✔
88
        self
3✔
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 {
4✔
98
        self.methods.push((func, flags));
12✔
99
        self
4✔
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>>(
2✔
118
        mut self,
119
        name: T,
120
        flags: PropertyFlags,
121
        docs: DocComments,
122
    ) -> Self {
123
        self.properties.push((name.into(), flags, docs));
10✔
124
        self
2✔
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>>(
1✔
143
        mut self,
144
        name: T,
145
        value: impl IntoZval + 'static,
146
        docs: DocComments,
147
    ) -> Result<Self> {
148
        self.constants
1✔
149
            .push((name.into(), Box::new(|| value.into_zval(true)), docs));
5✔
150
        Ok(self)
1✔
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>>(
1✔
169
        mut self,
170
        name: T,
171
        value: &'static dyn IntoZvalDyn,
172
        docs: DocComments,
173
    ) -> Result<Self> {
174
        let value = Rc::new(value);
3✔
175
        self.constants
1✔
176
            .push((name.into(), Box::new(move || value.as_zval(true)), docs));
5✔
177
        Ok(self)
1✔
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 {
2✔
186
        self.ce.ce_flags = flags.bits();
2✔
187
        self
2✔
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 {
1✔
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!(
1✔
242
            self.name.as_str(),
2✔
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>);
1✔
247
        let is_interface = T::FLAGS.contains(ClassFlags::Interface);
3✔
248

249
        let (func, visibility) = if let Some(ConstructorMeta {
2✔
250
            build_fn, flags, ..
×
251
        }) = T::constructor()
1✔
252
        {
NEW
253
            let func = if is_interface {
×
NEW
254
                FunctionBuilder::new_abstract("__construct")
×
255
            } else {
NEW
256
                FunctionBuilder::new("__construct", constructor::<T>)
×
257
            };
258

UNCOV
259
            (build_fn(func), flags.unwrap_or(MethodFlags::Public))
×
260
        } else {
261
            (
262
                if is_interface {
1✔
NEW
263
                    FunctionBuilder::new_abstract("__construct")
×
264
                } else {
265
                    FunctionBuilder::new("__construct", constructor::<T>)
1✔
266
                },
UNCOV
267
                MethodFlags::Public,
×
268
            )
269
        };
270

271
        self.method(func, visibility)
4✔
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 {
3✔
281
        self.register = Some(register);
3✔
282
        self
3✔
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 {
2✔
291
        self.docs = docs;
2✔
292
        self
2✔
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<()> {
1✔
308
        self.ce.name = ZendStr::new_interned(&self.name, true).into_raw();
2✔
309

310
        let mut methods = self
2✔
311
            .methods
1✔
312
            .into_iter()
313
            .map(|(method, flags)| {
3✔
314
                method.build().map(|mut method| {
8✔
315
                    method.flags |= flags.bits();
2✔
316
                    method
2✔
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) {
1✔
326
            unsafe {
NEW
327
                zend_register_internal_interface(&raw mut self.ce)
×
328
                    .as_mut()
NEW
329
                    .ok_or(Error::InvalidPointer)?
×
330
            }
331
        } else {
332
            unsafe {
333
                zend_register_internal_class_ex(
NEW
334
                    &raw mut self.ce,
×
NEW
335
                    match self.extends {
×
NEW
336
                        Some((ptr, _)) => ptr::from_ref(ptr()).cast_mut(),
×
337
                        None => std::ptr::null_mut(),
1✔
338
                    },
339
                )
340
                .as_mut()
NEW
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() {
1✔
347
            cfg_if::cfg_if! {
1✔
348
                if #[cfg(php81)] {
1✔
349
                    class.ce_flags |= ClassFlags::NotSerializable.bits();
1✔
350
                } else {
351
                    class.serialize = Some(crate::ffi::zend_class_serialize_deny);
1✔
352
                    class.unserialize = Some(crate::ffi::zend_class_unserialize_deny);
1✔
353
                }
354
            }
355
        }
356

357
        for (iface, _) in self.interfaces {
1✔
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 {
1✔
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 {
1✔
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 {
2✔
392
            class.__bindgen_anon_2.create_object = Some(object_override);
×
393
        }
394

395
        if let Some(register) = self.register {
2✔
396
            register(class);
1✔
397
        } else {
398
            panic!("Class {} was not registered.", self.name);
×
399
        }
400

401
        Ok(())
1✔
402
    }
403
}
404

405
#[cfg(test)]
406
mod 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
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc