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}