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

davidcole1340 / ext-php-rs / 16006197694

01 Jul 2025 05:33PM UTC coverage: 21.654% (-0.3%) from 21.978%
16006197694

Pull #471

github

web-flow
Merge 7af465c1e into 68e218f9b
Pull Request #471: feat(sapi): expand `SapiBuilder`

66 of 69 new or added lines in 1 file covered. (95.65%)

748 existing lines in 21 files now uncovered.

830 of 3833 relevant lines covered (21.65%)

3.63 hits per line

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

90.63
/src/exception.rs
1
//! Types and functions used for throwing exceptions from Rust to PHP.
2

3
use std::{ffi::CString, fmt::Debug, ptr};
4

5
use crate::{
6
    class::RegisteredClass,
7
    error::{Error, Result},
8
    ffi::zend_throw_exception_ex,
9
    ffi::zend_throw_exception_object,
10
    flags::ClassFlags,
11
    types::Zval,
12
    zend::{ce, ClassEntry},
13
};
14

15
/// Result type with the error variant as a [`PhpException`].
16
pub type PhpResult<T = ()> = std::result::Result<T, PhpException>;
17

18
/// Represents a PHP exception which can be thrown using the `throw()` function.
19
/// Primarily used to return from a [`Result<T, PhpException>`] which can
20
/// immediately be thrown by the `ext-php-rs` macro API.
21
///
22
/// There are default [`From`] implementations for any type that implements
23
/// [`ToString`], so these can also be returned from these functions. You can
24
/// also implement [`From<T>`] for your custom error type.
25
#[derive(Debug)]
26
pub struct PhpException {
27
    message: String,
28
    code: i32,
29
    ex: &'static ClassEntry,
30
    object: Option<Zval>,
31
}
32

33
impl PhpException {
34
    /// Creates a new exception instance.
35
    ///
36
    /// # Parameters
37
    ///
38
    /// * `message` - Message to contain in the exception.
39
    /// * `code` - Integer code to go inside the exception.
40
    /// * `ex` - Exception type to throw.
41
    #[must_use]
42
    pub fn new(message: String, code: i32, ex: &'static ClassEntry) -> Self {
9✔
43
        Self {
44
            message,
45
            code,
46
            ex,
47
            object: None,
48
        }
49
    }
50

51
    /// Creates a new default exception instance, using the default PHP
52
    /// `Exception` type as the exception type, with an integer code of
53
    /// zero.
54
    ///
55
    /// # Parameters
56
    ///
57
    /// * `message` - Message to contain in the exception.
58
    #[must_use]
59
    pub fn default(message: String) -> Self {
7✔
60
        Self::new(message, 0, ce::exception())
21✔
61
    }
62

63
    /// Creates an instance of an exception from a PHP class type and a message.
64
    ///
65
    /// # Parameters
66
    ///
67
    /// * `message` - Message to contain in the exception.
68
    #[must_use]
69
    pub fn from_class<T: RegisteredClass>(message: String) -> Self {
×
70
        Self::new(message, 0, T::get_metadata().ce())
×
71
    }
72

73
    /// Set the Zval object for the exception.
74
    ///
75
    /// Exceptions can be based of instantiated Zval objects when you are
76
    /// throwing a custom exception with stateful properties.
77
    ///
78
    /// # Parameters
79
    ///
80
    /// * `object` - The Zval object.
81
    pub fn set_object(&mut self, object: Option<Zval>) {
1✔
82
        self.object = object;
2✔
83
    }
84

85
    /// Builder function that sets the Zval object for the exception.
86
    ///
87
    /// Exceptions can be based of instantiated Zval objects when you are
88
    /// throwing a custom exception with stateful properties.
89
    ///
90
    /// # Parameters
91
    ///
92
    /// * `object` - The Zval object.
93
    #[must_use]
94
    pub fn with_object(mut self, object: Zval) -> Self {
2✔
95
        self.object = Some(object);
4✔
96
        self
2✔
97
    }
98

99
    /// Throws the exception, returning nothing inside a result if successful
100
    /// and an error otherwise.
101
    ///
102
    /// # Errors
103
    ///
104
    /// * [`Error::InvalidException`] - If the exception type is an interface or
105
    ///   abstract class.
106
    /// * If the message contains NUL bytes.
107
    pub fn throw(self) -> Result<()> {
2✔
108
        match self.object {
2✔
109
            Some(object) => throw_object(object),
3✔
110
            None => throw_with_code(self.ex, self.code, &self.message),
1✔
111
        }
112
    }
113
}
114

115
impl From<String> for PhpException {
116
    fn from(str: String) -> Self {
1✔
117
        Self::default(str)
2✔
118
    }
119
}
120

121
impl From<&str> for PhpException {
122
    fn from(str: &str) -> Self {
1✔
123
        Self::default(str.into())
3✔
124
    }
125
}
126

127
#[cfg(feature = "anyhow")]
128
impl From<anyhow::Error> for PhpException {
129
    fn from(err: anyhow::Error) -> Self {
1✔
130
        Self::new(format!("{err:#}"), 0, crate::zend::ce::exception())
4✔
131
    }
132
}
133

134
/// Throws an exception with a given message. See [`ClassEntry`] for some
135
/// built-in exception types.
136
///
137
/// Returns a result containing nothing if the exception was successfully
138
/// thrown.
139
///
140
/// # Parameters
141
///
142
/// * `ex` - The exception type to throw.
143
/// * `message` - The message to display when throwing the exception.
144
///
145
/// # Errors
146
///
147
/// * [`Error::InvalidException`] - If the exception type is an interface or
148
///   abstract class.
149
/// * If the message contains NUL bytes.
150
///
151
/// # Examples
152
///
153
/// ```no_run
154
/// use ext_php_rs::{zend::{ce, ClassEntry}, exception::throw};
155
///
156
/// throw(ce::compile_error(), "This is a CompileError.");
157
/// ```
158
pub fn throw(ex: &ClassEntry, message: &str) -> Result<()> {
1✔
159
    throw_with_code(ex, 0, message)
3✔
160
}
161

162
/// Throws an exception with a given message and status code. See [`ClassEntry`]
163
/// for some built-in exception types.
164
///
165
/// Returns a result containing nothing if the exception was successfully
166
/// thrown.
167
///
168
/// # Parameters
169
///
170
/// * `ex` - The exception type to throw.
171
/// * `code` - The status code to use when throwing the exception.
172
/// * `message` - The message to display when throwing the exception.
173
///
174
/// # Errors
175
///
176
/// * [`Error::InvalidException`] - If the exception type is an interface or
177
///   abstract class.
178
/// * If the message contains NUL bytes.
179
///
180
/// # Examples
181
///
182
/// ```no_run
183
/// use ext_php_rs::{zend::{ce, ClassEntry}, exception::throw_with_code};
184
///
185
/// throw_with_code(ce::compile_error(), 123, "This is a CompileError.");
186
/// ```
187
pub fn throw_with_code(ex: &ClassEntry, code: i32, message: &str) -> Result<()> {
4✔
188
    let flags = ex.flags();
12✔
189

190
    // Can't throw an interface or abstract class.
191
    if flags.contains(ClassFlags::Interface) || flags.contains(ClassFlags::Abstract) {
11✔
192
        return Err(Error::InvalidException(flags));
1✔
193
    }
194

195
    // SAFETY: We are given a reference to a `ClassEntry` therefore when we cast it
196
    // to a pointer it will be valid.
197
    unsafe {
198
        zend_throw_exception_ex(
199
            ptr::from_ref(ex).cast_mut(),
200
            code.into(),
201
            CString::new("%s")?.as_ptr(),
3✔
UNCOV
202
            CString::new(message)?.as_ptr(),
×
203
        )
204
    };
205
    Ok(())
206
}
207

208
/// Throws an exception object.
209
///
210
/// Returns a result containing nothing if the exception was successfully
211
/// thrown.
212
///
213
/// # Parameters
214
///
215
/// * `object` - The zval of type object
216
///
217
/// # Errors
218
///
219
/// *shrug*
220
/// TODO: does this error?
221
///
222
/// # Examples
223
///
224
/// ```no_run
225
/// use ext_php_rs::prelude::*;
226
/// use ext_php_rs::exception::throw_object;
227
/// use crate::ext_php_rs::convert::IntoZval;
228
///
229
/// #[php_class]
230
/// #[php(extends(ce = ext_php_rs::zend::ce::exception, stub = "\\Exception"))]
231
/// pub struct JsException {
232
///     #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)]
233
///     message: String,
234
///     #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)]
235
///     code: i32,
236
///     #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)]
237
///     file: String,
238
/// }
239
///
240
/// #[php_module]
241
/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
242
///     module
243
/// }
244
///
245
/// let error = JsException { message: "A JS error occurred.".to_string(), code: 100, file: "index.js".to_string() };
246
/// throw_object( error.into_zval(true).unwrap() );
247
/// ```
248
pub fn throw_object(zval: Zval) -> Result<()> {
2✔
249
    let mut zv = core::mem::ManuallyDrop::new(zval);
6✔
250
    unsafe { zend_throw_exception_object(core::ptr::addr_of_mut!(zv).cast()) };
6✔
251
    Ok(())
2✔
252
}
253

254
#[cfg(feature = "embed")]
255
#[cfg(test)]
256
mod tests {
257
    #![allow(clippy::assertions_on_constants)]
258
    use super::*;
259
    use crate::embed::Embed;
260

261
    #[test]
262
    fn test_new() {
263
        Embed::run(|| {
264
            let ex = PhpException::new("Test".into(), 0, ce::exception());
265
            assert_eq!(ex.message, "Test");
266
            assert_eq!(ex.code, 0);
267
            assert_eq!(ex.ex, ce::exception());
268
            assert!(ex.object.is_none());
269
        });
270
    }
271

272
    #[test]
273
    fn test_default() {
274
        Embed::run(|| {
275
            let ex = PhpException::default("Test".into());
276
            assert_eq!(ex.message, "Test");
277
            assert_eq!(ex.code, 0);
278
            assert_eq!(ex.ex, ce::exception());
279
            assert!(ex.object.is_none());
280
        });
281
    }
282

283
    #[test]
284
    fn test_set_object() {
285
        Embed::run(|| {
286
            let mut ex = PhpException::default("Test".into());
287
            assert!(ex.object.is_none());
288
            let obj = Zval::new();
289
            ex.set_object(Some(obj));
290
            assert!(ex.object.is_some());
291
        });
292
    }
293

294
    #[test]
295
    fn test_with_object() {
296
        Embed::run(|| {
297
            let obj = Zval::new();
298
            let ex = PhpException::default("Test".into()).with_object(obj);
299
            assert!(ex.object.is_some());
300
        });
301
    }
302

303
    #[test]
304
    fn test_throw_code() {
305
        Embed::run(|| {
306
            let ex = PhpException::default("Test".into());
307
            assert!(ex.throw().is_ok());
308

309
            assert!(false, "Should not reach here");
310
        });
311
    }
312

313
    #[test]
314
    fn test_throw_object() {
315
        Embed::run(|| {
316
            let ex = PhpException::default("Test".into()).with_object(Zval::new());
317
            assert!(ex.throw().is_ok());
318

319
            assert!(false, "Should not reach here");
320
        });
321
    }
322

323
    #[test]
324
    fn test_from_string() {
325
        Embed::run(|| {
326
            let ex: PhpException = "Test".to_string().into();
327
            assert_eq!(ex.message, "Test");
328
            assert_eq!(ex.code, 0);
329
            assert_eq!(ex.ex, ce::exception());
330
            assert!(ex.object.is_none());
331
        });
332
    }
333

334
    #[test]
335
    fn test_from_str() {
336
        Embed::run(|| {
337
            let ex: PhpException = "Test str".into();
338
            assert_eq!(ex.message, "Test str");
339
            assert_eq!(ex.code, 0);
340
            assert_eq!(ex.ex, ce::exception());
341
            assert!(ex.object.is_none());
342
        });
343
    }
344

345
    #[cfg(feature = "anyhow")]
346
    #[test]
347
    fn test_from_anyhow() {
348
        Embed::run(|| {
349
            let ex: PhpException = anyhow::anyhow!("Test anyhow").into();
350
            assert_eq!(ex.message, "Test anyhow");
351
            assert_eq!(ex.code, 0);
352
            assert_eq!(ex.ex, ce::exception());
353
            assert!(ex.object.is_none());
354
        });
355
    }
356

357
    #[test]
358
    fn test_throw_ex() {
359
        Embed::run(|| {
360
            assert!(throw(ce::exception(), "Test").is_ok());
361

362
            assert!(false, "Should not reach here");
363
        });
364
    }
365

366
    #[test]
367
    fn test_throw_with_code() {
368
        Embed::run(|| {
369
            assert!(throw_with_code(ce::exception(), 1, "Test").is_ok());
370

371
            assert!(false, "Should not reach here");
372
        });
373
    }
374

375
    // TODO: Test abstract class
376
    #[test]
377
    fn test_throw_with_code_interface() {
378
        Embed::run(|| {
379
            assert!(throw_with_code(ce::arrayaccess(), 0, "Test").is_err());
380
        });
381
    }
382

383
    #[test]
384
    fn test_static_throw_object() {
385
        Embed::run(|| {
386
            let obj = Zval::new();
387
            assert!(throw_object(obj).is_ok());
388

389
            assert!(false, "Should not reach here");
390
        });
391
    }
392
}
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