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}