use std::{ffi::CString, fmt::Debug, ptr};
use crate::{
class::RegisteredClass,
error::{Error, Result},
ffi::zend_throw_exception_ex,
ffi::zend_throw_exception_object,
flags::ClassFlags,
types::Zval,
zend::{ClassEntry, ce},
};
pub type PhpResult<T = ()> = std::result::Result<T, PhpException>;
#[derive(Debug)]
pub struct PhpException {
message: String,
code: i32,
ex: &'static ClassEntry,
object: Option<Zval>,
}
impl PhpException {
#[must_use]
pub fn new(message: String, code: i32, ex: &'static ClassEntry) -> Self {
Self {
message,
code,
ex,
object: None,
}
}
#[must_use]
pub fn default(message: String) -> Self {
Self::new(message, 0, ce::exception())
}
#[must_use]
pub fn from_class<T: RegisteredClass>(message: String) -> Self {
Self::new(message, 0, T::get_metadata().ce())
}
pub fn set_object(&mut self, object: Option<Zval>) {
self.object = object;
}
#[must_use]
pub fn with_object(mut self, object: Zval) -> Self {
self.object = Some(object);
self
}
pub fn throw(self) -> Result<()> {
match self.object {
Some(object) => throw_object(object),
None => throw_with_code(self.ex, self.code, &self.message),
}
}
}
impl From<String> for PhpException {
fn from(str: String) -> Self {
Self::default(str)
}
}
impl From<&str> for PhpException {
fn from(str: &str) -> Self {
Self::default(str.into())
}
}
#[cfg(feature = "anyhow")]
impl From<anyhow::Error> for PhpException {
fn from(err: anyhow::Error) -> Self {
Self::new(format!("{err:#}"), 0, crate::zend::ce::exception())
}
}
pub fn throw(ex: &ClassEntry, message: &str) -> Result<()> {
throw_with_code(ex, 0, message)
}
pub fn throw_with_code(ex: &ClassEntry, code: i32, message: &str) -> Result<()> {
let flags = ex.flags();
if flags.contains(ClassFlags::Interface) || flags.contains(ClassFlags::Abstract) {
return Err(Error::InvalidException(flags));
}
unsafe {
zend_throw_exception_ex(
ptr::from_ref(ex).cast_mut(),
code.into(),
CString::new("%s")?.as_ptr(),
CString::new(message)?.as_ptr(),
)
};
Ok(())
}
pub fn throw_object(zval: Zval) -> Result<()> {
let mut zv = core::mem::ManuallyDrop::new(zval);
unsafe { zend_throw_exception_object(core::ptr::addr_of_mut!(zv).cast()) };
Ok(())
}
#[cfg(feature = "embed")]
#[cfg(test)]
mod tests {
#![allow(clippy::assertions_on_constants)]
use super::*;
use crate::embed::Embed;
#[test]
fn test_new() {
Embed::run(|| {
let ex = PhpException::new("Test".into(), 0, ce::exception());
assert_eq!(ex.message, "Test");
assert_eq!(ex.code, 0);
assert_eq!(ex.ex, ce::exception());
assert!(ex.object.is_none());
});
}
#[test]
fn test_default() {
Embed::run(|| {
let ex = PhpException::default("Test".into());
assert_eq!(ex.message, "Test");
assert_eq!(ex.code, 0);
assert_eq!(ex.ex, ce::exception());
assert!(ex.object.is_none());
});
}
#[test]
fn test_set_object() {
Embed::run(|| {
let mut ex = PhpException::default("Test".into());
assert!(ex.object.is_none());
let obj = Zval::new();
ex.set_object(Some(obj));
assert!(ex.object.is_some());
});
}
#[test]
fn test_with_object() {
Embed::run(|| {
let obj = Zval::new();
let ex = PhpException::default("Test".into()).with_object(obj);
assert!(ex.object.is_some());
});
}
#[test]
fn test_throw_code() {
Embed::run(|| {
let ex = PhpException::default("Test".into());
assert!(ex.throw().is_ok());
assert!(false, "Should not reach here");
});
}
#[test]
fn test_throw_object() {
Embed::run(|| {
let ex = PhpException::default("Test".into()).with_object(Zval::new());
assert!(ex.throw().is_ok());
assert!(false, "Should not reach here");
});
}
#[test]
fn test_from_string() {
Embed::run(|| {
let ex: PhpException = "Test".to_string().into();
assert_eq!(ex.message, "Test");
assert_eq!(ex.code, 0);
assert_eq!(ex.ex, ce::exception());
assert!(ex.object.is_none());
});
}
#[test]
fn test_from_str() {
Embed::run(|| {
let ex: PhpException = "Test str".into();
assert_eq!(ex.message, "Test str");
assert_eq!(ex.code, 0);
assert_eq!(ex.ex, ce::exception());
assert!(ex.object.is_none());
});
}
#[cfg(feature = "anyhow")]
#[test]
fn test_from_anyhow() {
Embed::run(|| {
let ex: PhpException = anyhow::anyhow!("Test anyhow").into();
assert_eq!(ex.message, "Test anyhow");
assert_eq!(ex.code, 0);
assert_eq!(ex.ex, ce::exception());
assert!(ex.object.is_none());
});
}
#[test]
fn test_throw_ex() {
Embed::run(|| {
assert!(throw(ce::exception(), "Test").is_ok());
assert!(false, "Should not reach here");
});
}
#[test]
fn test_throw_with_code() {
Embed::run(|| {
assert!(throw_with_code(ce::exception(), 1, "Test").is_ok());
assert!(false, "Should not reach here");
});
}
#[test]
fn test_throw_with_code_interface() {
Embed::run(|| {
assert!(throw_with_code(ce::arrayaccess(), 0, "Test").is_err());
});
}
#[test]
fn test_static_throw_object() {
Embed::run(|| {
let obj = Zval::new();
assert!(throw_object(obj).is_ok());
assert!(false, "Should not reach here");
});
}
}