Skip to main content

ext_php_rs/types/
callable.rs

1//! Types related to callables in PHP (anonymous functions, functions, etc).
2
3use std::{convert::TryFrom, ops::Deref, ptr};
4
5use crate::{
6    convert::{FromZval, IntoZvalDyn},
7    error::{Error, Result},
8    ffi::_call_user_function_impl,
9    flags::DataType,
10    zend::ExecutorGlobals,
11};
12
13use super::{ZendHashTable, Zval};
14
15/// Acts as a wrapper around a callable [`Zval`]. Allows the owner to call the
16/// [`Zval`] as if it was a PHP function through the [`try_call`] method.
17///
18/// [`try_call`]: #method.try_call
19#[derive(Debug)]
20pub struct ZendCallable<'a>(OwnedZval<'a>);
21
22impl<'a> ZendCallable<'a> {
23    /// Attempts to create a new [`ZendCallable`] from a zval.
24    ///
25    /// # Parameters
26    ///
27    /// * `callable` - The underlying [`Zval`] that is callable.
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if the [`Zval`] was not callable.
32    pub fn new(callable: &'a Zval) -> Result<Self> {
33        if callable.is_callable() {
34            Ok(Self(OwnedZval::Reference(callable)))
35        } else {
36            Err(Error::Callable)
37        }
38    }
39
40    /// Attempts to create a new [`ZendCallable`] by taking ownership of a Zval.
41    /// Returns a result containing the callable if the zval was callable.
42    ///
43    /// # Parameters
44    ///
45    /// * `callable` - The underlying [`Zval`] that is callable.
46    ///
47    /// # Errors
48    ///
49    /// * [`Error::Callable`] - If the zval was not callable.
50    pub fn new_owned(callable: Zval) -> Result<Self> {
51        if callable.is_callable() {
52            Ok(Self(OwnedZval::Owned(callable)))
53        } else {
54            Err(Error::Callable)
55        }
56    }
57
58    /// Attempts to create a new [`ZendCallable`] from a function name. Returns
59    /// a result containing the callable if the function existed and was
60    /// callable.
61    ///
62    /// # Parameters
63    ///
64    /// * `name` - Name of the callable function.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the function does not exist or is not callable.
69    ///
70    /// # Example
71    ///
72    /// ```no_run
73    /// use ext_php_rs::types::ZendCallable;
74    ///
75    /// let strpos = ZendCallable::try_from_name("strpos").unwrap();
76    /// let result = strpos.try_call(vec![&"hello", &"e"]).unwrap();
77    /// assert_eq!(result.long(), Some(1));
78    /// ```
79    pub fn try_from_name(name: &str) -> Result<Self> {
80        let mut callable = Zval::new();
81        callable.set_string(name, false)?;
82
83        Self::new_owned(callable)
84    }
85
86    /// Attempts to call the callable with a list of arguments to pass to the
87    /// function.
88    ///
89    /// You should not call this function directly, rather through the
90    /// [`call_user_func`] macro.
91    ///
92    /// # Parameters
93    ///
94    /// * `params` - A list of parameters to call the function with.
95    ///
96    /// # Returns
97    ///
98    /// Returns the result wrapped in [`Ok`] upon success.
99    ///
100    /// # Errors
101    ///
102    /// * If calling the callable fails, or an exception is thrown, an [`Err`]
103    ///   is returned.
104    /// * If the number of parameters exceeds `u32::MAX`.
105    ///
106    /// # Example
107    ///
108    /// ```no_run
109    /// use ext_php_rs::types::ZendCallable;
110    ///
111    /// let strpos = ZendCallable::try_from_name("strpos").unwrap();
112    /// let result = strpos.try_call(vec![&"hello", &"e"]).unwrap();
113    /// assert_eq!(result.long(), Some(1));
114    /// ```
115    // TODO: Measure this
116    #[allow(clippy::inline_always)]
117    #[inline(always)]
118    pub fn try_call(&self, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
119        if !self.0.is_callable() {
120            return Err(Error::Callable);
121        }
122
123        let mut retval = Zval::new();
124        let len = params.len();
125        let params = params
126            .into_iter()
127            .map(|val| val.as_zval(false))
128            .collect::<Result<Vec<_>>>()?;
129        let packed = params.into_boxed_slice();
130
131        let result = unsafe {
132            #[allow(clippy::used_underscore_items)]
133            _call_user_function_impl(
134                ptr::null_mut(),
135                ptr::from_ref(self.0.as_ref()).cast_mut(),
136                &raw mut retval,
137                len.try_into()?,
138                packed.as_ptr().cast_mut(),
139                ptr::null_mut(),
140            )
141        };
142
143        if result < 0 {
144            Err(Error::Callable)
145        } else if let Some(e) = ExecutorGlobals::take_exception() {
146            Err(Error::Exception(e))
147        } else {
148            Ok(retval)
149        }
150    }
151
152    /// Attempts to call the callable with both positional and named arguments.
153    ///
154    /// This method supports PHP 8.0+ named arguments, allowing you to pass
155    /// arguments by name rather than position. Named arguments are passed
156    /// after positional arguments.
157    ///
158    /// # Parameters
159    ///
160    /// * `params` - A list of positional parameters to call the function with.
161    /// * `named_params` - A list of named parameters as (name, value) tuples.
162    ///
163    /// # Returns
164    ///
165    /// Returns the result wrapped in [`Ok`] upon success.
166    ///
167    /// # Errors
168    ///
169    /// * If calling the callable fails, or an exception is thrown, an [`Err`]
170    ///   is returned.
171    /// * If the number of parameters exceeds `u32::MAX`.
172    /// * If a parameter name contains a NUL byte.
173    ///
174    /// # Example
175    ///
176    /// ```no_run
177    /// use ext_php_rs::types::ZendCallable;
178    ///
179    /// // Call str_replace with named arguments
180    /// let str_replace = ZendCallable::try_from_name("str_replace").unwrap();
181    /// let result = str_replace.try_call_with_named(
182    ///     &[],  // no positional args
183    ///     &[("search", &"world"), ("replace", &"PHP"), ("subject", &"Hello world")],
184    /// ).unwrap();
185    /// assert_eq!(result.string(), Some("Hello PHP".into()));
186    /// ```
187    // TODO: Measure this
188    #[allow(clippy::inline_always)]
189    #[inline(always)]
190    pub fn try_call_with_named(
191        &self,
192        params: &[&dyn IntoZvalDyn],
193        named_params: &[(&str, &dyn IntoZvalDyn)],
194    ) -> Result<Zval> {
195        if !self.0.is_callable() {
196            return Err(Error::Callable);
197        }
198
199        let mut retval = Zval::new();
200        let len = params.len();
201        let params = params
202            .iter()
203            .map(|val| val.as_zval(false))
204            .collect::<Result<Vec<_>>>()?;
205        let packed = params.into_boxed_slice();
206
207        // Build the named parameters hash table
208        let named_ht = if named_params.is_empty() {
209            None
210        } else {
211            let mut ht = ZendHashTable::with_capacity(named_params.len().try_into()?);
212            for &(name, val) in named_params {
213                let zval = val.as_zval(false)?;
214                ht.insert(name, zval)?;
215            }
216            Some(ht)
217        };
218
219        let named_ptr = named_ht
220            .as_ref()
221            .map_or(ptr::null_mut(), |ht| ptr::from_ref(&**ht).cast_mut());
222
223        let result = unsafe {
224            #[allow(clippy::used_underscore_items)]
225            _call_user_function_impl(
226                ptr::null_mut(),
227                ptr::from_ref(self.0.as_ref()).cast_mut(),
228                &raw mut retval,
229                len.try_into()?,
230                packed.as_ptr().cast_mut(),
231                named_ptr,
232            )
233        };
234
235        if result < 0 {
236            Err(Error::Callable)
237        } else if let Some(e) = ExecutorGlobals::take_exception() {
238            Err(Error::Exception(e))
239        } else {
240            Ok(retval)
241        }
242    }
243
244    /// Attempts to call the callable with only named arguments.
245    ///
246    /// This is a convenience method equivalent to calling
247    /// [`try_call_with_named`] with an empty positional arguments vector.
248    ///
249    /// # Parameters
250    ///
251    /// * `named_params` - A list of named parameters as (name, value) tuples.
252    ///
253    /// # Returns
254    ///
255    /// Returns the result wrapped in [`Ok`] upon success.
256    ///
257    /// # Errors
258    ///
259    /// * If calling the callable fails, or an exception is thrown, an [`Err`]
260    ///   is returned.
261    /// * If a parameter name contains a NUL byte.
262    ///
263    /// # Example
264    ///
265    /// ```no_run
266    /// use ext_php_rs::types::ZendCallable;
267    ///
268    /// // Call array_fill with named arguments only
269    /// let array_fill = ZendCallable::try_from_name("array_fill").unwrap();
270    /// let result = array_fill.try_call_named(&[
271    ///     ("start_index", &0i64),
272    ///     ("count", &3i64),
273    ///     ("value", &"PHP"),
274    /// ]).unwrap();
275    /// ```
276    ///
277    /// [`try_call_with_named`]: #method.try_call_with_named
278    #[inline]
279    pub fn try_call_named(&self, named_params: &[(&str, &dyn IntoZvalDyn)]) -> Result<Zval> {
280        self.try_call_with_named(&[], named_params)
281    }
282}
283
284impl<'a> FromZval<'a> for ZendCallable<'a> {
285    const TYPE: DataType = DataType::Callable;
286
287    fn from_zval(zval: &'a Zval) -> Option<Self> {
288        ZendCallable::new(zval).ok()
289    }
290}
291
292impl TryFrom<Zval> for ZendCallable<'_> {
293    type Error = Error;
294
295    fn try_from(value: Zval) -> Result<Self> {
296        ZendCallable::new_owned(value)
297    }
298}
299
300/// A container for a zval. Either contains a reference to a zval or an owned
301/// zval.
302#[derive(Debug)]
303enum OwnedZval<'a> {
304    Reference(&'a Zval),
305    Owned(Zval),
306}
307
308impl OwnedZval<'_> {
309    fn as_ref(&self) -> &Zval {
310        match self {
311            OwnedZval::Reference(zv) => zv,
312            OwnedZval::Owned(zv) => zv,
313        }
314    }
315}
316
317impl Deref for OwnedZval<'_> {
318    type Target = Zval;
319
320    fn deref(&self) -> &Self::Target {
321        self.as_ref()
322    }
323}