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

davidcole1340 / ext-php-rs / 15778794992

20 Jun 2025 12:19PM UTC coverage: 20.64% (-1.4%) from 22.034%
15778794992

Pull #463

github

web-flow
Merge b618ded48 into 660f308c0
Pull Request #463: feat(cargo-php): --features, --all-features, --no-default-features

0 of 11 new or added lines in 1 file covered. (0.0%)

52 existing lines in 10 files now uncovered.

761 of 3687 relevant lines covered (20.64%)

3.57 hits per line

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

57.14
/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,
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 {
12✔
45
        Self {
46
            name: name.into(),
36✔
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() },
36✔
50
            extends: None,
51
            interfaces: vec![],
24✔
52
            methods: vec![],
24✔
53
            object_override: None,
54
            properties: vec![],
24✔
55
            constants: vec![],
24✔
56
            register: None,
57
            docs: &[],
12✔
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 {
2✔
67
        self.extends = Some(parent);
2✔
68
        self
2✔
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 {
3✔
81
        self.interfaces.push(interface);
9✔
82
        self
3✔
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 {
4✔
92
        self.methods.push((func, flags));
12✔
93
        self
4✔
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>>(
2✔
112
        mut self,
113
        name: T,
114
        flags: PropertyFlags,
115
        docs: DocComments,
116
    ) -> Self {
117
        self.properties.push((name.into(), flags, docs));
10✔
118
        self
2✔
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>>(
1✔
137
        mut self,
138
        name: T,
139
        value: impl IntoZval + 'static,
140
        docs: DocComments,
141
    ) -> Result<Self> {
142
        self.constants
1✔
143
            .push((name.into(), Box::new(|| value.into_zval(true)), docs));
5✔
144
        Ok(self)
1✔
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>>(
1✔
163
        mut self,
164
        name: T,
165
        value: &'static dyn IntoZvalDyn,
166
        docs: DocComments,
167
    ) -> Result<Self> {
168
        let value = Rc::new(value);
3✔
169
        self.constants
1✔
170
            .push((name.into(), Box::new(move || value.as_zval(true)), docs));
5✔
171
        Ok(self)
1✔
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 {
1✔
180
        self.ce.ce_flags = flags.bits();
1✔
181
        self
1✔
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 {
1✔
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!(
1✔
236
            self.name.as_str(),
2✔
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>);
1✔
241
        self.method(
2✔
242
            {
243
                let mut func = FunctionBuilder::new("__construct", constructor::<T>);
3✔
244
                if let Some(ConstructorMeta { build_fn, .. }) = T::constructor() {
1✔
245
                    func = build_fn(func);
×
246
                }
247
                func
1✔
248
            },
UNCOV
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 {
2✔
260
        self.register = Some(register);
2✔
261
        self
2✔
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 {
2✔
270
        self.docs = docs;
2✔
271
        self
2✔
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<()> {
1✔
287
        self.ce.name = ZendStr::new_interned(&self.name, true).into_raw();
2✔
288

289
        let mut methods = self
2✔
290
            .methods
1✔
291
            .into_iter()
292
            .map(|(method, flags)| {
3✔
293
                method.build().map(|mut method| {
8✔
294
                    method.flags |= flags.bits();
2✔
295
                    method
2✔
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
                &mut self.ce,
×
307
                match self.extends {
×
308
                    Some((ptr, _)) => ptr::from_ref(ptr()).cast_mut(),
×
309
                    None => std::ptr::null_mut(),
1✔
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() {
1✔
318
            cfg_if::cfg_if! {
1✔
319
                if #[cfg(php81)] {
1✔
320
                    class.ce_flags |= ClassFlags::NotSerializable.bits();
1✔
321
                } else {
322
                    class.serialize = Some(crate::ffi::zend_class_serialize_deny);
1✔
323
                    class.unserialize = Some(crate::ffi::zend_class_unserialize_deny);
1✔
324
                }
325
            }
326
        }
327

328
        for (iface, _) in self.interfaces {
1✔
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 {
1✔
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 {
1✔
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 {
2✔
363
            class.__bindgen_anon_2.create_object = Some(object_override);
×
364
        }
365

366
        if let Some(register) = self.register {
2✔
367
            register(class);
1✔
368
        } else {
369
            panic!("Class {} was not registered.", self.name);
×
370
        }
371

372
        Ok(())
1✔
373
    }
374
}
375

376
#[cfg(test)]
377
mod 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
}
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