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