Skip to main content

ext_php_rs/zend/
error_observer.rs

1//! Zend Observer API bindings for PHP error observation.
2//!
3//! Enables creation of error loggers, APM integrations, and monitoring tools.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use ext_php_rs::prelude::*;
9//!
10//! struct MyErrorLogger;
11//!
12//! impl ErrorObserver for MyErrorLogger {
13//!     fn should_observe(&self, error_type: ErrorType) -> bool {
14//!         ErrorType::Fatal.contains(error_type)
15//!     }
16//!
17//!     fn on_error(&self, error: &ErrorInfo) {
18//!         eprintln!("[{}:{}] {}",
19//!             error.filename.unwrap_or("<unknown>"),
20//!             error.lineno,
21//!             error.message
22//!         );
23//!     }
24//! }
25//! ```
26
27use std::sync::OnceLock;
28
29use crate::ffi;
30pub use crate::flags::ErrorType;
31
32/// A single frame in a PHP backtrace.
33#[derive(Debug, Clone)]
34pub struct BacktraceFrame {
35    /// Function name (`None` for main script).
36    pub function: Option<String>,
37    /// Class name for method calls.
38    pub class: Option<String>,
39    /// Source file.
40    pub file: Option<String>,
41    /// Line number.
42    pub line: u32,
43}
44
45impl BacktraceFrame {
46    /// # Safety
47    ///
48    /// `execute_data` must be a valid pointer.
49    pub(crate) unsafe fn from_execute_data(
50        execute_data: *const ffi::zend_execute_data,
51    ) -> Option<Self> {
52        if execute_data.is_null() {
53            return None;
54        }
55
56        let ex = unsafe { &*execute_data };
57        let func = ex.func;
58        if func.is_null() {
59            return Some(Self {
60                function: None,
61                class: None,
62                file: None,
63                line: 0,
64            });
65        }
66
67        let common = unsafe { &(*func).common };
68
69        let function = if common.function_name.is_null() {
70            None
71        } else {
72            unsafe { zend_string_to_string(common.function_name) }
73        };
74
75        let class = if common.scope.is_null() {
76            None
77        } else {
78            let ce = unsafe { &*common.scope };
79            if ce.name.is_null() {
80                None
81            } else {
82                unsafe { zend_string_to_string(ce.name) }
83            }
84        };
85
86        #[allow(clippy::cast_possible_truncation)]
87        let is_internal = common.type_ == ffi::ZEND_INTERNAL_FUNCTION as u8;
88
89        let (file, line) = if is_internal {
90            (None, ex.opline as u32)
91        } else {
92            let op_array = unsafe { &(*func).op_array };
93            let file = if op_array.filename.is_null() {
94                None
95            } else {
96                unsafe { zend_string_to_string(op_array.filename) }
97            };
98            // Use current opline if available, otherwise use function start line
99            let line = if ex.opline.is_null() {
100                op_array.line_start
101            } else {
102                unsafe { (*ex.opline).lineno }
103            };
104            (file, line)
105        };
106
107        Some(Self {
108            function,
109            class,
110            file,
111            line,
112        })
113    }
114}
115
116/// Information about a PHP error passed to observers.
117#[derive(Debug)]
118pub struct ErrorInfo<'a> {
119    /// The error type/severity level.
120    pub error_type: ErrorType,
121    /// Source filename where the error occurred.
122    pub filename: Option<&'a str>,
123    /// Line number where the error occurred.
124    pub lineno: u32,
125    /// The error message.
126    pub message: &'a str,
127}
128
129impl ErrorInfo<'_> {
130    /// Captures the current PHP call stack.
131    ///
132    /// This is lazy - only captures when called, zero cost if unused.
133    /// Returns `None` if no execution context is available.
134    ///
135    /// # Example
136    ///
137    /// ```ignore
138    /// fn on_error(&self, error: &ErrorInfo) {
139    ///     if let Some(trace) = error.backtrace() {
140    ///         for frame in trace {
141    ///             eprintln!("  at {}:{}",
142    ///                 frame.file.as_deref().unwrap_or("<internal>"),
143    ///                 frame.line
144    ///             );
145    ///         }
146    ///     }
147    /// }
148    /// ```
149    #[must_use]
150    pub fn backtrace(&self) -> Option<Vec<BacktraceFrame>> {
151        let eg = unsafe { crate::ffi::ext_php_rs_executor_globals().as_ref()? };
152        let mut execute_data = eg.current_execute_data;
153
154        let mut frames = Vec::new();
155        while !execute_data.is_null() {
156            if let Some(frame) = unsafe { BacktraceFrame::from_execute_data(execute_data) } {
157                frames.push(frame);
158            }
159            execute_data = unsafe { (*execute_data).prev_execute_data };
160        }
161
162        if frames.is_empty() {
163            None
164        } else {
165            Some(frames)
166        }
167    }
168}
169
170/// Trait for observing PHP errors.
171///
172/// # Lifecycle
173///
174/// 1. `should_observe` is called for each error to filter by type
175/// 2. If `true`, `on_error` is called with error details
176///
177/// # Thread Safety
178///
179/// Observer must be `Send + Sync`. Use thread-safe primitives for mutable state.
180///
181/// # Example
182///
183/// ```ignore
184/// use ext_php_rs::prelude::*;
185/// use std::sync::atomic::{AtomicU64, Ordering};
186///
187/// struct ErrorCounter {
188///     count: AtomicU64,
189/// }
190///
191/// impl ErrorObserver for ErrorCounter {
192///     fn should_observe(&self, error_type: ErrorType) -> bool {
193///         ErrorType::Fatal.contains(error_type)
194///     }
195///
196///     fn on_error(&self, error: &ErrorInfo) {
197///         self.count.fetch_add(1, Ordering::Relaxed);
198///     }
199/// }
200/// ```
201pub trait ErrorObserver: 'static {
202    /// Filter which error types to observe.
203    ///
204    /// Called for every error. Return `true` to receive `on_error` callback.
205    /// Use bitflags for efficient filtering.
206    fn should_observe(&self, error_type: ErrorType) -> bool;
207
208    /// Called when an observed error occurs.
209    fn on_error(&self, error: &ErrorInfo);
210}
211
212type ErrorObserverFactory = Box<dyn Fn() -> Box<dyn ErrorObserver + Send + Sync> + Send + Sync>;
213
214static ERROR_OBSERVER_FACTORY: OnceLock<ErrorObserverFactory> = OnceLock::new();
215static ERROR_OBSERVER_INSTANCE: OnceLock<Box<dyn ErrorObserver + Send + Sync>> = OnceLock::new();
216
217fn get_error_observer() -> Option<&'static (dyn ErrorObserver + Send + Sync)> {
218    ERROR_OBSERVER_INSTANCE
219        .get()
220        .map(std::convert::AsRef::as_ref)
221}
222
223/// # Safety
224///
225/// Pointer must be valid and string must be valid UTF-8.
226unsafe fn zend_string_to_str(zs: *mut ffi::zend_string) -> Option<&'static str> {
227    if zs.is_null() {
228        return None;
229    }
230    let len = unsafe { (*zs).len };
231    let ptr = unsafe { (*zs).val.as_ptr() };
232    let slice = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
233    std::str::from_utf8(slice).ok()
234}
235
236/// # Safety
237///
238/// Pointer must be valid and string must be valid UTF-8.
239unsafe fn zend_string_to_string(zs: *mut ffi::zend_string) -> Option<String> {
240    unsafe { zend_string_to_str(zs) }.map(String::from)
241}
242
243/// # Safety
244///
245/// Called from PHP's C code. In ZTS mode, called from the correct thread context.
246unsafe extern "C" fn error_observer_callback(
247    error_type: std::ffi::c_int,
248    error_filename: *mut ffi::zend_string,
249    error_lineno: u32,
250    message: *mut ffi::zend_string,
251) {
252    let Some(observer) = get_error_observer() else {
253        return;
254    };
255
256    let error_type = ErrorType::from_bits_truncate(error_type.cast_unsigned());
257
258    if !observer.should_observe(error_type) {
259        return;
260    }
261
262    let filename = unsafe { zend_string_to_str(error_filename) };
263    let message_str = unsafe { zend_string_to_str(message) }.unwrap_or("");
264
265    let info = ErrorInfo {
266        error_type,
267        filename,
268        lineno: error_lineno,
269        message: message_str,
270    };
271
272    observer.on_error(&info);
273}
274
275/// # Panics
276///
277/// Panics if called more than once.
278pub(crate) fn register_error_observer_factory(factory: ErrorObserverFactory) {
279    assert!(
280        ERROR_OBSERVER_FACTORY.set(factory).is_ok(),
281        "error_observer can only be registered once per extension"
282    );
283}
284
285/// # Safety
286///
287/// Must be called during MINIT phase only.
288pub(crate) unsafe fn error_observer_startup() {
289    let Some(factory) = ERROR_OBSERVER_FACTORY.get() else {
290        return;
291    };
292    ERROR_OBSERVER_INSTANCE.get_or_init(factory);
293    unsafe { ffi::zend_observer_error_register(Some(error_observer_callback)) };
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    struct TestErrorObserver {
301        observe_all: bool,
302    }
303
304    unsafe impl Send for TestErrorObserver {}
305    unsafe impl Sync for TestErrorObserver {}
306
307    impl ErrorObserver for TestErrorObserver {
308        fn should_observe(&self, _error_type: ErrorType) -> bool {
309            self.observe_all
310        }
311
312        fn on_error(&self, _error: &ErrorInfo) {}
313    }
314
315    #[test]
316    fn test_error_type_bitflags() {
317        assert!(ErrorType::Fatal.contains(ErrorType::Error));
318        assert!(ErrorType::Fatal.contains(ErrorType::Parse));
319        assert!(!ErrorType::Fatal.contains(ErrorType::Warning));
320        assert!(!ErrorType::Fatal.contains(ErrorType::Notice));
321    }
322
323    #[test]
324    fn test_error_type_all() {
325        assert!(ErrorType::All.contains(ErrorType::Error));
326        assert!(ErrorType::All.contains(ErrorType::Warning));
327        assert!(ErrorType::All.contains(ErrorType::Notice));
328        assert!(ErrorType::All.contains(ErrorType::Deprecated));
329    }
330
331    #[test]
332    #[allow(deprecated)]
333    fn test_error_type_all_matches_php_e_all() {
334        assert_eq!(ErrorType::All.contains(ErrorType::Strict), !cfg!(php84));
335    }
336
337    #[test]
338    fn test_observer_trait_impl() {
339        let observer = TestErrorObserver { observe_all: true };
340        assert!(observer.should_observe(ErrorType::Error));
341
342        let observer = TestErrorObserver { observe_all: false };
343        assert!(!observer.should_observe(ErrorType::Error));
344    }
345
346    #[test]
347    fn test_error_type_from_bits() {
348        let error_type = ErrorType::from_bits_truncate(1); // E_ERROR
349        assert_eq!(error_type, ErrorType::Error);
350
351        let error_type = ErrorType::from_bits_truncate(2); // E_WARNING
352        assert_eq!(error_type, ErrorType::Warning);
353
354        let error_type = ErrorType::from_bits_truncate(3); // E_ERROR | E_WARNING
355        assert!(error_type.contains(ErrorType::Error));
356        assert!(error_type.contains(ErrorType::Warning));
357    }
358}