Skip to main content

ext_php_rs/
exception.rs

1//! Types and functions used for throwing exceptions from Rust to PHP.
2
3use std::{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::{ZendStr, Zval},
12    zend::{ClassEntry, ExecutorGlobals, ce},
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/// [`From`] is implemented for [`String`], [`&str`], [`crate::error::Error`]
23/// and, behind the `anyhow` feature, `anyhow::Error`. Implement [`From<T>`]
24/// for your own error type to return it from these functions.
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 exception instance from a message, 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 from_message(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. Always leaves an exception pending in the engine.
100    ///
101    /// Does nothing if an exception is already pending: the engine propagates
102    /// that one, and throwing over it would discard its class and stack trace.
103    ///
104    /// When the exception itself cannot be thrown (an interface or abstract
105    /// class, a message containing a NUL byte, or an attached value that is not
106    /// an object) a plain `Error` is thrown instead. Its message names the
107    /// cause and carries the original message, so the failure is reported to
108    /// PHP rather than to the Rust caller, which sits inside an `extern "C"`
109    /// frame with no way to propagate it.
110    pub fn throw(self) {
111        if ExecutorGlobals::has_exception() {
112            return;
113        }
114
115        let class = self.ex.name().unwrap_or_default();
116        let message = self.message.replace('\0', "\\0");
117        let result = match self.object {
118            Some(object) => throw_object(object),
119            None => throw_with_code(self.ex, self.code, &self.message),
120        };
121
122        if let Err(err) = result {
123            let fallback = format!("cannot throw {class}: {err}; original message: {message}");
124            let _ = throw_with_code(ce::error(), 0, &fallback);
125        }
126    }
127}
128
129impl From<String> for PhpException {
130    fn from(str: String) -> Self {
131        Self::from_message(str)
132    }
133}
134
135impl From<&str> for PhpException {
136    fn from(str: &str) -> Self {
137        Self::from_message(str.into())
138    }
139}
140
141#[cfg(feature = "anyhow")]
142impl From<anyhow::Error> for PhpException {
143    fn from(err: anyhow::Error) -> Self {
144        Self::new(format!("{err:#}"), 0, crate::zend::ce::exception())
145    }
146}
147
148/// Throws an exception with a given message. See [`ClassEntry`] for some
149/// built-in exception types.
150///
151/// Returns a result containing nothing if the exception was successfully
152/// thrown.
153///
154/// # Parameters
155///
156/// * `ex` - The exception type to throw.
157/// * `message` - The message to display when throwing the exception.
158///
159/// # Errors
160///
161/// * [`Error::InvalidException`] - If the exception type is an interface or
162///   abstract class.
163/// * If the message contains NUL bytes.
164///
165/// # Examples
166///
167/// ```no_run
168/// use ext_php_rs::{zend::{ce, ClassEntry}, exception::throw};
169///
170/// throw(ce::compile_error(), "This is a CompileError.");
171/// ```
172pub fn throw(ex: &ClassEntry, message: &str) -> Result<()> {
173    throw_with_code(ex, 0, message)
174}
175
176/// Throws an exception with a given message and status code. See [`ClassEntry`]
177/// for some built-in exception types.
178///
179/// Returns a result containing nothing if the exception was successfully
180/// thrown.
181///
182/// # Parameters
183///
184/// * `ex` - The exception type to throw.
185/// * `code` - The status code to use when throwing the exception.
186/// * `message` - The message to display when throwing the exception.
187///
188/// # Errors
189///
190/// * [`Error::InvalidException`] - If the exception type is an interface or
191///   abstract class.
192/// * If the message contains NUL bytes.
193///
194/// # Examples
195///
196/// ```no_run
197/// use ext_php_rs::{zend::{ce, ClassEntry}, exception::throw_with_code};
198///
199/// throw_with_code(ce::compile_error(), 123, "This is a CompileError.");
200/// ```
201pub fn throw_with_code(ex: &ClassEntry, code: i32, message: &str) -> Result<()> {
202    let flags = ex.flags();
203
204    // Can't throw an interface or abstract class.
205    if flags.contains(ClassFlags::Interface) || flags.contains(ClassFlags::Abstract) {
206        return Err(Error::InvalidException(flags));
207    }
208
209    let message = ZendStr::new(message, false);
210    let message_ptr = message.as_c_str()?.as_ptr();
211
212    // SAFETY: We are given a reference to a `ClassEntry` therefore when we cast it
213    // to a pointer it will be valid. `message` is NUL-terminated and outlives the
214    // call; if the engine bails out here the Zend allocator reclaims it at request
215    // shutdown, as php-src does for its own copy of the message.
216    unsafe {
217        zend_throw_exception_ex(
218            ptr::from_ref(ex).cast_mut(),
219            code.into(),
220            c"%s".as_ptr(),
221            message_ptr,
222        )
223    };
224    Ok(())
225}
226
227/// Throws an exception object.
228///
229/// Returns a result containing nothing if the exception was successfully
230/// thrown.
231///
232/// # Parameters
233///
234/// * `object` - The zval of type object
235///
236/// # Errors
237///
238/// * [`Error::Object`] - If the zval does not hold an object.
239///
240/// # Examples
241///
242/// ```no_run
243/// use ext_php_rs::prelude::*;
244/// use ext_php_rs::exception::throw_object;
245/// use crate::ext_php_rs::convert::IntoZval;
246///
247/// #[php_class]
248/// #[php(extends(ce = ext_php_rs::zend::ce::exception, stub = "\\Exception"))]
249/// pub struct JsException {
250///     #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)]
251///     message: String,
252///     #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)]
253///     code: i32,
254///     #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)]
255///     file: String,
256/// }
257///
258/// #[php_module]
259/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
260///     module
261/// }
262///
263/// let error = JsException { message: "A JS error occurred.".to_string(), code: 100, file: "index.js".to_string() };
264/// throw_object( error.into_zval(true).unwrap() );
265/// ```
266pub fn throw_object(zval: Zval) -> Result<()> {
267    if !zval.is_object() {
268        return Err(Error::Object);
269    }
270
271    let mut zv = core::mem::ManuallyDrop::new(zval);
272    unsafe { zend_throw_exception_object(core::ptr::addr_of_mut!(zv).cast()) };
273    Ok(())
274}
275
276#[cfg(feature = "embed")]
277#[cfg(test)]
278mod tests {
279    #![allow(clippy::assertions_on_constants)]
280    use super::*;
281    use crate::embed::Embed;
282
283    #[test]
284    fn test_new() {
285        Embed::run(|| {
286            let ex = PhpException::new("Test".into(), 0, ce::exception());
287            assert_eq!(ex.message, "Test");
288            assert_eq!(ex.code, 0);
289            assert_eq!(ex.ex, ce::exception());
290            assert!(ex.object.is_none());
291        });
292    }
293
294    #[test]
295    fn test_default() {
296        Embed::run(|| {
297            let ex = PhpException::from_message("Test".into());
298            assert_eq!(ex.message, "Test");
299            assert_eq!(ex.code, 0);
300            assert_eq!(ex.ex, ce::exception());
301            assert!(ex.object.is_none());
302        });
303    }
304
305    #[test]
306    fn test_set_object() {
307        Embed::run(|| {
308            let mut ex = PhpException::from_message("Test".into());
309            assert!(ex.object.is_none());
310            let obj = Zval::new();
311            ex.set_object(Some(obj));
312            assert!(ex.object.is_some());
313        });
314    }
315
316    #[test]
317    fn test_with_object() {
318        Embed::run(|| {
319            let obj = Zval::new();
320            let ex = PhpException::from_message("Test".into()).with_object(obj);
321            assert!(ex.object.is_some());
322        });
323    }
324
325    #[test]
326    fn test_from_string() {
327        Embed::run(|| {
328            let ex: PhpException = "Test".to_string().into();
329            assert_eq!(ex.message, "Test");
330            assert_eq!(ex.code, 0);
331            assert_eq!(ex.ex, ce::exception());
332            assert!(ex.object.is_none());
333        });
334    }
335
336    #[test]
337    fn test_from_str() {
338        Embed::run(|| {
339            let ex: PhpException = "Test str".into();
340            assert_eq!(ex.message, "Test str");
341            assert_eq!(ex.code, 0);
342            assert_eq!(ex.ex, ce::exception());
343            assert!(ex.object.is_none());
344        });
345    }
346
347    #[cfg(feature = "anyhow")]
348    #[test]
349    fn test_from_anyhow() {
350        Embed::run(|| {
351            let ex: PhpException = anyhow::anyhow!("Test anyhow").into();
352            assert_eq!(ex.message, "Test anyhow");
353            assert_eq!(ex.code, 0);
354            assert_eq!(ex.ex, ce::exception());
355            assert!(ex.object.is_none());
356        });
357    }
358
359    #[test]
360    fn test_throw_ex() {
361        Embed::run(|| {
362            assert!(throw(ce::exception(), "Test").is_ok());
363
364            assert!(false, "Should not reach here");
365        });
366    }
367
368    #[test]
369    fn test_throw_with_code() {
370        Embed::run(|| {
371            assert!(throw_with_code(ce::exception(), 1, "Test").is_ok());
372
373            assert!(false, "Should not reach here");
374        });
375    }
376
377    // TODO: Test abstract class
378    #[test]
379    fn test_throw_with_code_interface() {
380        Embed::run(|| {
381            assert!(throw_with_code(ce::arrayaccess(), 0, "Test").is_err());
382        });
383    }
384
385    #[test]
386    fn test_static_throw_object_rejects_a_non_object() {
387        Embed::run(|| {
388            assert!(matches!(throw_object(Zval::new()), Err(Error::Object)));
389        });
390    }
391}