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,
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 result = match self.object {
116 Some(object) => throw_object(object),
117 None => throw_with_code(self.ex, self.code, &self.message),
118 };
119
120 if let Err(err) = result {
121 let class = self.ex.name().unwrap_or_default();
122 let message = self.message.replace('\0', "\\0");
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 { zend_throw_exception(ptr::from_ref(ex).cast_mut(), message_ptr, code.into()) };
217 Ok(())
218}
219
220/// Throws an exception object.
221///
222/// Returns a result containing nothing if the exception was successfully
223/// thrown.
224///
225/// # Parameters
226///
227/// * `object` - The zval of type object
228///
229/// # Errors
230///
231/// * [`Error::Object`] - If the zval does not hold an object.
232///
233/// # Examples
234///
235/// ```no_run
236/// use ext_php_rs::prelude::*;
237/// use ext_php_rs::exception::throw_object;
238/// use crate::ext_php_rs::convert::IntoZval;
239///
240/// #[php_class]
241/// #[php(extends(ce = ext_php_rs::zend::ce::exception, stub = "\\Exception"))]
242/// pub struct JsException {
243/// #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)]
244/// message: String,
245/// #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)]
246/// code: i32,
247/// #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)]
248/// file: String,
249/// }
250///
251/// #[php_module]
252/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
253/// module
254/// }
255///
256/// let error = JsException { message: "A JS error occurred.".to_string(), code: 100, file: "index.js".to_string() };
257/// throw_object( error.into_zval(true).unwrap() );
258/// ```
259pub fn throw_object(zval: Zval) -> Result<()> {
260 if !zval.is_object() {
261 return Err(Error::Object);
262 }
263
264 let mut zv = core::mem::ManuallyDrop::new(zval);
265 unsafe { zend_throw_exception_object(core::ptr::addr_of_mut!(zv).cast()) };
266 Ok(())
267}
268
269#[cfg(feature = "embed")]
270#[cfg(test)]
271mod tests {
272 #![allow(clippy::assertions_on_constants)]
273 use super::*;
274 use crate::embed::Embed;
275
276 #[test]
277 fn test_new() {
278 Embed::run(|| {
279 let ex = PhpException::new("Test".into(), 0, ce::exception());
280 assert_eq!(ex.message, "Test");
281 assert_eq!(ex.code, 0);
282 assert_eq!(ex.ex, ce::exception());
283 assert!(ex.object.is_none());
284 });
285 }
286
287 #[test]
288 fn test_default() {
289 Embed::run(|| {
290 let ex = PhpException::from_message("Test".into());
291 assert_eq!(ex.message, "Test");
292 assert_eq!(ex.code, 0);
293 assert_eq!(ex.ex, ce::exception());
294 assert!(ex.object.is_none());
295 });
296 }
297
298 #[test]
299 fn test_set_object() {
300 Embed::run(|| {
301 let mut ex = PhpException::from_message("Test".into());
302 assert!(ex.object.is_none());
303 let obj = Zval::new();
304 ex.set_object(Some(obj));
305 assert!(ex.object.is_some());
306 });
307 }
308
309 #[test]
310 fn test_with_object() {
311 Embed::run(|| {
312 let obj = Zval::new();
313 let ex = PhpException::from_message("Test".into()).with_object(obj);
314 assert!(ex.object.is_some());
315 });
316 }
317
318 #[test]
319 fn test_from_string() {
320 Embed::run(|| {
321 let ex: PhpException = "Test".to_string().into();
322 assert_eq!(ex.message, "Test");
323 assert_eq!(ex.code, 0);
324 assert_eq!(ex.ex, ce::exception());
325 assert!(ex.object.is_none());
326 });
327 }
328
329 #[test]
330 fn test_from_str() {
331 Embed::run(|| {
332 let ex: PhpException = "Test str".into();
333 assert_eq!(ex.message, "Test str");
334 assert_eq!(ex.code, 0);
335 assert_eq!(ex.ex, ce::exception());
336 assert!(ex.object.is_none());
337 });
338 }
339
340 #[cfg(feature = "anyhow")]
341 #[test]
342 fn test_from_anyhow() {
343 Embed::run(|| {
344 let ex: PhpException = anyhow::anyhow!("Test anyhow").into();
345 assert_eq!(ex.message, "Test anyhow");
346 assert_eq!(ex.code, 0);
347 assert_eq!(ex.ex, ce::exception());
348 assert!(ex.object.is_none());
349 });
350 }
351
352 #[test]
353 fn test_throw_ex() {
354 Embed::run(|| {
355 assert!(throw(ce::exception(), "Test").is_ok());
356
357 assert!(false, "Should not reach here");
358 });
359 }
360
361 #[test]
362 fn test_throw_with_code() {
363 Embed::run(|| {
364 assert!(throw_with_code(ce::exception(), 1, "Test").is_ok());
365
366 assert!(false, "Should not reach here");
367 });
368 }
369
370 // TODO: Test abstract class
371 #[test]
372 fn test_throw_with_code_interface() {
373 Embed::run(|| {
374 assert!(throw_with_code(ce::arrayaccess(), 0, "Test").is_err());
375 });
376 }
377
378 #[test]
379 fn test_static_throw_object_rejects_a_non_object() {
380 Embed::run(|| {
381 assert!(matches!(throw_object(Zval::new()), Err(Error::Object)));
382 });
383 }
384}