Skip to content

Commit e229700

Browse files
committed
feat(callable): Named arguments
1 parent 6523879 commit e229700

File tree

6 files changed

+303
-4
lines changed

6 files changed

+303
-4
lines changed

crates/macros/src/function.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,8 @@ fn expr_to_php_stub(expr: &Expr) -> String {
845845
}
846846
}
847847

848-
/// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
848+
/// Returns true if the given type is nullable in PHP (i.e., it's an
849+
/// `Option<T>`).
849850
///
850851
/// Note: Having a default value does NOT make a type nullable. A parameter with
851852
/// a default value is optional (can be omitted), but passing `null` explicitly

guide/src/types/closure.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,54 @@ pub fn callable_parameter(call: ZendCallable) {
123123
}
124124
# fn main() {}
125125
```
126+
127+
### Named Arguments (PHP 8.0+)
128+
129+
You can call PHP functions with named arguments using `try_call_named` or
130+
`try_call_with_named`. Named arguments allow you to pass parameters by name
131+
rather than position, which is especially useful when dealing with functions
132+
that have many optional parameters.
133+
134+
```rust,no_run
135+
# #![cfg_attr(windows, feature(abi_vectorcall))]
136+
# extern crate ext_php_rs;
137+
use ext_php_rs::prelude::*;
138+
use ext_php_rs::call_user_func_named;
139+
140+
#[php_function]
141+
pub fn call_with_named_args() -> String {
142+
// Get str_replace function
143+
let str_replace = ZendCallable::try_from_name("str_replace")
144+
.expect("str_replace not found");
145+
146+
// Call with named arguments in any order
147+
let result = str_replace.try_call_named(vec![
148+
("subject", &"Hello world"),
149+
("search", &"world"),
150+
("replace", &"PHP"),
151+
]).expect("Failed to call str_replace");
152+
153+
result.string().unwrap_or_default()
154+
}
155+
156+
#[php_function]
157+
pub fn call_with_mixed_args(callback: ZendCallable) {
158+
// Mix positional and named arguments
159+
let result = callback.try_call_with_named(
160+
vec![&"positional_arg"], // positional args first
161+
vec![("named", &"named_value")], // then named args
162+
).expect("Failed to call function");
163+
dbg!(result);
164+
}
165+
# fn main() {}
166+
```
167+
168+
There's also a convenient `call_user_func_named!` macro:
169+
170+
```rust,ignore
171+
// Named arguments only
172+
call_user_func_named!(callable, arg1: value1, arg2: value2)?;
173+
174+
// Positional arguments followed by named arguments
175+
call_user_func_named!(callable, [pos1, pos2], named1: val1, named2: val2)?;
176+
```

src/macros.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,70 @@ macro_rules! call_user_func {
7272
};
7373
}
7474

75+
/// Attempts to call a given PHP callable with named arguments.
76+
///
77+
/// This macro supports PHP 8.0+ named arguments, allowing you to pass
78+
/// arguments by name rather than position.
79+
///
80+
/// # Syntax
81+
///
82+
/// ```ignore
83+
/// // Named arguments only
84+
/// call_user_func_named!(callable, name1: value1, name2: value2)
85+
///
86+
/// // Positional arguments followed by named arguments
87+
/// call_user_func_named!(callable, [pos1, pos2], name1: value1, name2: value2)
88+
/// ```
89+
///
90+
/// # Parameters
91+
///
92+
/// * `$fn` - The 'function' to call. Can be an [`Arg`] or a [`Zval`].
93+
/// * `$name: $value` - Named parameters as `name: value` pairs.
94+
/// * `[$($pos),*]` - Optional positional parameters in square brackets.
95+
///
96+
/// # Examples
97+
///
98+
/// ```ignore
99+
/// use ext_php_rs::{call_user_func_named, types::ZendCallable};
100+
///
101+
/// let str_replace = ZendCallable::try_from_name("str_replace").unwrap();
102+
///
103+
/// // Using named arguments only
104+
/// let result = call_user_func_named!(str_replace,
105+
/// search: "world",
106+
/// replace: "PHP",
107+
/// subject: "Hello world"
108+
/// ).unwrap();
109+
///
110+
/// // Mixing positional and named arguments
111+
/// let result = call_user_func_named!(str_replace, ["world", "PHP"],
112+
/// subject: "Hello world"
113+
/// ).unwrap();
114+
/// ```
115+
///
116+
/// [`Arg`]: crate::args::Arg
117+
/// [`Zval`]: crate::types::Zval
118+
#[macro_export]
119+
macro_rules! call_user_func_named {
120+
// Named arguments only
121+
($fn: expr, $($name: ident : $value: expr),+ $(,)?) => {
122+
$fn.try_call_named(vec![$((stringify!($name), &$value as &dyn $crate::convert::IntoZvalDyn)),+])
123+
};
124+
125+
// Positional arguments followed by named arguments
126+
($fn: expr, [$($pos: expr),* $(,)?], $($name: ident : $value: expr),+ $(,)?) => {
127+
$fn.try_call_with_named(
128+
vec![$(&$pos as &dyn $crate::convert::IntoZvalDyn),*],
129+
vec![$((stringify!($name), &$value as &dyn $crate::convert::IntoZvalDyn)),+]
130+
)
131+
};
132+
133+
// Only positional arguments (fallback to regular call)
134+
($fn: expr, [$($pos: expr),* $(,)?] $(,)?) => {
135+
$fn.try_call(vec![$(&$pos as &dyn $crate::convert::IntoZvalDyn),*])
136+
};
137+
}
138+
75139
/// Parses a given list of arguments using the [`ArgParser`] class.
76140
///
77141
/// # Examples

src/types/callable.rs

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::{
1010
zend::ExecutorGlobals,
1111
};
1212

13-
use super::Zval;
13+
use super::{ZendHashTable, Zval};
1414

1515
/// Acts as a wrapper around a callable [`Zval`]. Allows the owner to call the
1616
/// [`Zval`] as if it was a PHP function through the [`try_call`] method.
@@ -148,6 +148,137 @@ impl<'a> ZendCallable<'a> {
148148
Ok(retval)
149149
}
150150
}
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+
/// vec![], // no positional args
183+
/// vec![("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: Vec<&dyn IntoZvalDyn>,
193+
named_params: Vec<(&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+
.into_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::new();
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(vec![
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: Vec<(&str, &dyn IntoZvalDyn)>) -> Result<Zval> {
280+
self.try_call_with_named(vec![], named_params)
281+
}
151282
}
152283

153284
impl<'a> FromZval<'a> for ZendCallable<'a> {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
11
<?php
22

3+
// Basic callable test
34
assert(test_callable(fn (string $a) => $a, 'test') === 'test');
5+
6+
// Named arguments test - order should not matter, args matched by name
7+
$namedResult = test_callable_named(fn (string $a, string $b) => "$a-$b");
8+
assert($namedResult === 'first-second', "Named args failed: expected 'first-second', got '$namedResult'");
9+
10+
// Mixed positional + named arguments test
11+
$mixedResult = test_callable_mixed(fn (string $pos, string $named) => "$pos|$named");
12+
assert($mixedResult === 'positional|named_value', "Mixed args failed: expected 'positional|named_value', got '$mixedResult'");
13+
14+
// Macro test with named arguments only
15+
$macroNamedResult = test_callable_macro_named(fn (string $x, string $y) => "$x $y");
16+
assert($macroNamedResult === 'hello world', "Macro named args failed: expected 'hello world', got '$macroNamedResult'");
17+
18+
// Macro test with positional + named arguments
19+
$macroMixedResult = test_callable_macro_mixed(fn (string $first, string $second) => "$first,$second");
20+
assert($macroMixedResult === 'first,second_val', "Macro mixed args failed: expected 'first,second_val', got '$macroMixedResult'");
21+
22+
// Test with built-in PHP function using named args via ZendCallable::try_from_name
23+
// This tests str_replace with named arguments in a different order

tests/src/integration/callable/mod.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,44 @@
1-
use ext_php_rs::{prelude::*, types::Zval};
1+
use ext_php_rs::{call_user_func_named, prelude::*, types::Zval};
22

33
#[php_function]
44
pub fn test_callable(call: ZendCallable, a: String) -> Zval {
55
call.try_call(vec![&a]).expect("Failed to call function")
66
}
77

8+
/// Test calling a callable with only named arguments
9+
#[php_function]
10+
pub fn test_callable_named(call: ZendCallable) -> Zval {
11+
call.try_call_named(vec![("b", &"second"), ("a", &"first")])
12+
.expect("Failed to call function with named args")
13+
}
14+
15+
/// Test calling a callable with positional + named arguments
16+
#[php_function]
17+
pub fn test_callable_mixed(call: ZendCallable) -> Zval {
18+
call.try_call_with_named(vec![&"positional"], vec![("named", &"named_value")])
19+
.expect("Failed to call function with mixed args")
20+
}
21+
22+
/// Test the `call_user_func_named!` macro with named arguments only
23+
#[php_function]
24+
pub fn test_callable_macro_named(call: ZendCallable) -> Zval {
25+
call_user_func_named!(call, x: "hello", y: "world").expect("Failed to call function via macro")
26+
}
27+
28+
/// Test the `call_user_func_named!` macro with positional + named arguments
29+
#[php_function]
30+
pub fn test_callable_macro_mixed(call: ZendCallable) -> Zval {
31+
call_user_func_named!(call, ["first"], second: "second_val")
32+
.expect("Failed to call function via macro with mixed args")
33+
}
34+
835
pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
9-
builder.function(wrap_function!(test_callable))
36+
builder
37+
.function(wrap_function!(test_callable))
38+
.function(wrap_function!(test_callable_named))
39+
.function(wrap_function!(test_callable_mixed))
40+
.function(wrap_function!(test_callable_macro_named))
41+
.function(wrap_function!(test_callable_macro_mixed))
1042
}
1143

1244
#[cfg(test)]

0 commit comments

Comments
 (0)