ext_php_rs/
exception.rs

1//! Types and functions used for throwing exceptions from Rust to PHP.
2
3use std::{ffi::CString, fmt::Debug};
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    pub fn new(message: String, code: i32, ex: &'static ClassEntry) -> Self {
42        Self {
43            message,
44            code,
45            ex,
46            object: None,
47        }
48    }
49
50    /// Creates a new default exception instance, using the default PHP
51    /// `Exception` type as the exception type, with an integer code of
52    /// zero.
53    ///
54    /// # Parameters
55    ///
56    /// * `message` - Message to contain in the exception.
57    pub fn default(message: String) -> Self {
58        Self::new(message, 0, ce::exception())
59    }
60
61    /// Creates an instance of an exception from a PHP class type and a message.
62    ///
63    /// # Parameters
64    ///
65    /// * `message` - Message to contain in the exception.
66    pub fn from_class<T: RegisteredClass>(message: String) -> Self {
67        Self::new(message, 0, T::get_metadata().ce())
68    }
69
70    /// Set the Zval object for the exception.
71    ///
72    /// Exceptions can be based of instantiated Zval objects when you are
73    /// throwing a custom exception with stateful properties.
74    ///
75    /// # Parameters
76    ///
77    /// * `object` - The Zval object.
78    pub fn set_object(&mut self, object: Option<Zval>) {
79        self.object = object;
80    }
81
82    /// Throws the exception, returning nothing inside a result if successful
83    /// and an error otherwise.
84    pub fn throw(self) -> Result<()> {
85        match self.object {
86            Some(object) => throw_object(object),
87            None => throw_with_code(self.ex, self.code, &self.message),
88        }
89    }
90}
91
92impl From<String> for PhpException {
93    fn from(str: String) -> Self {
94        Self::default(str)
95    }
96}
97
98impl From<&str> for PhpException {
99    fn from(str: &str) -> Self {
100        Self::default(str.into())
101    }
102}
103
104#[cfg(feature = "anyhow")]
105impl From<anyhow::Error> for PhpException {
106    fn from(err: anyhow::Error) -> Self {
107        Self::new(format!("{:#}", err), 0, crate::zend::ce::exception())
108    }
109}
110
111/// Throws an exception with a given message. See [`ClassEntry`] for some
112/// built-in exception types.
113///
114/// Returns a result containing nothing if the exception was successfully
115/// thrown.
116///
117/// # Parameters
118///
119/// * `ex` - The exception type to throw.
120/// * `message` - The message to display when throwing the exception.
121///
122/// # Examples
123///
124/// ```no_run
125/// use ext_php_rs::{zend::{ce, ClassEntry}, exception::throw};
126///
127/// throw(ce::compile_error(), "This is a CompileError.");
128/// ```
129pub fn throw(ex: &ClassEntry, message: &str) -> Result<()> {
130    throw_with_code(ex, 0, message)
131}
132
133/// Throws an exception with a given message and status code. See [`ClassEntry`]
134/// for some built-in exception types.
135///
136/// Returns a result containing nothing if the exception was successfully
137/// thrown.
138///
139/// # Parameters
140///
141/// * `ex` - The exception type to throw.
142/// * `code` - The status code to use when throwing the exception.
143/// * `message` - The message to display when throwing the exception.
144///
145/// # Examples
146///
147/// ```no_run
148/// use ext_php_rs::{zend::{ce, ClassEntry}, exception::throw_with_code};
149///
150/// throw_with_code(ce::compile_error(), 123, "This is a CompileError.");
151/// ```
152pub fn throw_with_code(ex: &ClassEntry, code: i32, message: &str) -> Result<()> {
153    let flags = ex.flags();
154
155    // Can't throw an interface or abstract class.
156    if flags.contains(ClassFlags::Interface) || flags.contains(ClassFlags::Abstract) {
157        return Err(Error::InvalidException(flags));
158    }
159
160    // SAFETY: We are given a reference to a `ClassEntry` therefore when we cast it
161    // to a pointer it will be valid.
162    unsafe {
163        zend_throw_exception_ex(
164            (ex as *const _) as *mut _,
165            code as _,
166            CString::new("%s")?.as_ptr(),
167            CString::new(message)?.as_ptr(),
168        )
169    };
170    Ok(())
171}
172
173/// Throws an exception object.
174///
175/// Returns a result containing nothing if the exception was successfully
176/// thrown.
177///
178/// # Parameters
179///
180/// * `object` - The zval of type object
181///
182/// # Examples
183///
184/// ```no_run
185/// use ext_php_rs::prelude::*;
186/// use ext_php_rs::exception::throw_object;
187/// use crate::ext_php_rs::convert::IntoZval;
188///
189/// #[php_class]
190/// #[extends(ext_php_rs::zend::ce::exception())]
191/// pub struct JsException {
192///     #[prop(flags = ext_php_rs::flags::PropertyFlags::Public)]
193///     message: String,
194///     #[prop(flags = ext_php_rs::flags::PropertyFlags::Public)]
195///     code: i32,
196///     #[prop(flags = ext_php_rs::flags::PropertyFlags::Public)]
197///     file: String,
198/// }
199///
200/// #[php_module]
201/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
202///     module
203/// }
204///
205/// let error = JsException { message: "A JS error occurred.".to_string(), code: 100, file: "index.js".to_string() };
206/// throw_object( error.into_zval(true).unwrap() );
207/// ```
208pub fn throw_object(zval: Zval) -> Result<()> {
209    let mut zv = core::mem::ManuallyDrop::new(zval);
210    unsafe { zend_throw_exception_object(core::ptr::addr_of_mut!(zv).cast()) };
211    Ok(())
212}