ext_php_rs/
exception.rs

1//! Types and functions used for throwing exceptions from Rust to PHP.
2
3use std::{ffi::CString, fmt::Debug, ptr};
4
5use 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`].
16pub 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)]
26pub struct PhpException {
27    message: String,
28    code: i32,
29    ex: &'static ClassEntry,
30    object: Option<Zval>,
31}
32
33impl 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 {
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 {
60        Self::new(message, 0, ce::exception())
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>) {
82        self.object = object;
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 {
95        self.object = Some(object);
96        self
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<()> {
108        match self.object {
109            Some(object) => throw_object(object),
110            None => throw_with_code(self.ex, self.code, &self.message),
111        }
112    }
113}
114
115impl From<String> for PhpException {
116    fn from(str: String) -> Self {
117        Self::default(str)
118    }
119}
120
121impl From<&str> for PhpException {
122    fn from(str: &str) -> Self {
123        Self::default(str.into())
124    }
125}
126
127#[cfg(feature = "anyhow")]
128impl From<anyhow::Error> for PhpException {
129    fn from(err: anyhow::Error) -> Self {
130        Self::new(format!("{err:#}"), 0, crate::zend::ce::exception())
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/// ```
158pub fn throw(ex: &ClassEntry, message: &str) -> Result<()> {
159    throw_with_code(ex, 0, message)
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/// ```
187pub fn throw_with_code(ex: &ClassEntry, code: i32, message: &str) -> Result<()> {
188    let flags = ex.flags();
189
190    // Can't throw an interface or abstract class.
191    if flags.contains(ClassFlags::Interface) || flags.contains(ClassFlags::Abstract) {
192        return Err(Error::InvalidException(flags));
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(),
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/// ```
248pub fn throw_object(zval: Zval) -> Result<()> {
249    let mut zv = core::mem::ManuallyDrop::new(zval);
250    unsafe { zend_throw_exception_object(core::ptr::addr_of_mut!(zv).cast()) };
251    Ok(())
252}
253
254#[cfg(feature = "embed")]
255#[cfg(test)]
256mod 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}