Skip to content

Commit ca8aa7c

Browse files
committed
feat(class): Getter/setter implementation #325
1 parent 6b192e8 commit ca8aa7c

File tree

7 files changed

+280
-14
lines changed

7 files changed

+280
-14
lines changed

crates/macros/src/class.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,25 @@ fn generate_registered_class_impl(
203203
&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>
204204
> {
205205
use ::std::iter::FromIterator;
206-
::std::collections::HashMap::from_iter([
206+
use ::ext_php_rs::internal::class::PhpClassImpl;
207+
208+
// Start with field properties
209+
let mut props = ::std::collections::HashMap::from_iter([
207210
#(#fields,)*
208-
])
211+
]);
212+
213+
// Add method properties (from #[php(getter)] and #[php(setter)])
214+
let method_props = ::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default()
215+
.get_method_props();
216+
for (name, prop) in method_props {
217+
props.insert(name, ::ext_php_rs::internal::property::PropertyInfo {
218+
prop,
219+
flags: ::ext_php_rs::flags::PropertyFlags::Public,
220+
docs: &[],
221+
});
222+
}
223+
224+
props
209225
}
210226

211227
#[inline]

crates/macros/src/impl_.rs

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ impl MethodArgs {
114114
}
115115
}
116116

117+
/// A property getter or setter method.
118+
#[derive(Debug)]
119+
struct PropertyMethod<'a> {
120+
/// Property name in PHP (e.g., "name" for `get_name`/`set_name`).
121+
prop_name: String,
122+
/// The Rust method identifier.
123+
method_ident: &'a syn::Ident,
124+
/// Whether this is a getter (true) or setter (false).
125+
is_getter: bool,
126+
}
127+
117128
#[derive(Debug)]
118129
struct ParsedImpl<'a> {
119130
path: &'a syn::Path,
@@ -122,6 +133,8 @@ struct ParsedImpl<'a> {
122133
functions: Vec<FnBuilder>,
123134
constructor: Option<(Function<'a>, Option<Visibility>)>,
124135
constants: Vec<Constant<'a>>,
136+
/// Property getter/setter methods.
137+
properties: Vec<PropertyMethod<'a>>,
125138
}
126139

127140
#[derive(Debug, Eq, Hash, PartialEq)]
@@ -176,6 +189,7 @@ impl<'a> ParsedImpl<'a> {
176189
functions: Vec::default(),
177190
constructor: Option::default(),
178191
constants: Vec::default(),
192+
properties: Vec::default(),
179193
}
180194
}
181195

@@ -206,6 +220,32 @@ impl<'a> ParsedImpl<'a> {
206220
method.attrs.retain(|attr| !attr.path().is_ident("php"));
207221

208222
let opts = MethodArgs::new(name, attr);
223+
224+
// Handle getter/setter methods
225+
if matches!(opts.ty, MethodTy::Getter | MethodTy::Setter) {
226+
let is_getter = matches!(opts.ty, MethodTy::Getter);
227+
// Extract property name by stripping get_/set_ prefix
228+
let method_name = method.sig.ident.to_string();
229+
let prop_name = if is_getter {
230+
method_name
231+
.strip_prefix("get_")
232+
.unwrap_or(&method_name)
233+
.to_string()
234+
} else {
235+
method_name
236+
.strip_prefix("set_")
237+
.unwrap_or(&method_name)
238+
.to_string()
239+
};
240+
241+
self.properties.push(PropertyMethod {
242+
prop_name,
243+
method_ident: &method.sig.ident,
244+
is_getter,
245+
});
246+
continue;
247+
}
248+
209249
let args = Args::parse_from_fnargs(method.sig.inputs.iter(), opts.defaults)?;
210250
let mut func = Function::new(&method.sig, opts.name, args, opts.optional, docs);
211251

@@ -275,6 +315,59 @@ impl<'a> ParsedImpl<'a> {
275315
}
276316
});
277317

318+
// Group properties by name to combine getters and setters
319+
let mut prop_groups: HashMap<&str, (Option<&syn::Ident>, Option<&syn::Ident>)> =
320+
HashMap::new();
321+
for prop in &self.properties {
322+
let entry = prop_groups.entry(&prop.prop_name).or_default();
323+
if prop.is_getter {
324+
entry.0 = Some(prop.method_ident);
325+
} else {
326+
entry.1 = Some(prop.method_ident);
327+
}
328+
}
329+
330+
// Generate property creation code
331+
let property_inserts: Vec<TokenStream> = prop_groups
332+
.iter()
333+
.map(|(prop_name, (getter, setter))| {
334+
match (getter, setter) {
335+
(Some(getter_ident), Some(setter_ident)) => {
336+
// Both getter and setter - use combine
337+
quote! {
338+
props.insert(
339+
#prop_name,
340+
::ext_php_rs::props::Property::method_getter(#path::#getter_ident)
341+
.combine(::ext_php_rs::props::Property::method_setter(#path::#setter_ident))
342+
);
343+
}
344+
}
345+
(Some(getter_ident), None) => {
346+
// Only getter
347+
quote! {
348+
props.insert(
349+
#prop_name,
350+
::ext_php_rs::props::Property::method_getter(#path::#getter_ident)
351+
);
352+
}
353+
}
354+
(None, Some(setter_ident)) => {
355+
// Only setter
356+
quote! {
357+
props.insert(
358+
#prop_name,
359+
::ext_php_rs::props::Property::method_setter(#path::#setter_ident)
360+
);
361+
}
362+
}
363+
(None, None) => {
364+
// Should not happen
365+
quote! {}
366+
}
367+
}
368+
})
369+
.collect();
370+
278371
quote! {
279372
impl ::ext_php_rs::internal::class::PhpClassImpl<#path>
280373
for ::ext_php_rs::internal::class::PhpClassImplCollector<#path>
@@ -286,7 +379,9 @@ impl<'a> ParsedImpl<'a> {
286379
}
287380

288381
fn get_method_props<'a>(self) -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, #path>> {
289-
todo!()
382+
let mut props = ::std::collections::HashMap::new();
383+
#(#property_inserts)*
384+
props
290385
}
291386

292387
fn get_constructor(self) -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<#path>> {

crates/macros/tests/expand/class.expanded.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,24 @@ impl ::ext_php_rs::class::RegisteredClass for MyClass {
2525
::ext_php_rs::internal::property::PropertyInfo<'a, Self>,
2626
> {
2727
use ::std::iter::FromIterator;
28-
::std::collections::HashMap::from_iter([])
28+
use ::ext_php_rs::internal::class::PhpClassImpl;
29+
let mut props = ::std::collections::HashMap::from_iter([]);
30+
let method_props = ::ext_php_rs::internal::class::PhpClassImplCollector::<
31+
Self,
32+
>::default()
33+
.get_method_props();
34+
for (name, prop) in method_props {
35+
props
36+
.insert(
37+
name,
38+
::ext_php_rs::internal::property::PropertyInfo {
39+
prop,
40+
flags: ::ext_php_rs::flags::PropertyFlags::Public,
41+
docs: &[],
42+
},
43+
);
44+
}
45+
props
2946
}
3047
#[inline]
3148
fn method_builders() -> ::std::vec::Vec<

src/props.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,121 @@ impl<'a, T: 'a> Property<'a, T> {
167167
Self::Method { get, set }
168168
}
169169

170+
/// Creates a getter-only method property.
171+
///
172+
/// This is useful when the getter returns a type that only implements
173+
/// [`IntoZval`] but not [`FromZval`] for all lifetimes, such as
174+
/// `&'static str`.
175+
///
176+
/// # Parameters
177+
///
178+
/// * `get` - Function used to get the value of the property.
179+
///
180+
/// # Examples
181+
///
182+
/// ```no_run
183+
/// # use ext_php_rs::props::Property;
184+
/// struct Test;
185+
///
186+
/// impl Test {
187+
/// pub fn get_prop(&self) -> &'static str {
188+
/// "Hello"
189+
/// }
190+
/// }
191+
///
192+
/// let prop: Property<Test> = Property::method_getter(Test::get_prop);
193+
/// ```
194+
#[must_use]
195+
pub fn method_getter<V>(get: fn(&T) -> V) -> Self
196+
where
197+
V: IntoZval + 'a,
198+
{
199+
let get = Box::new(move |self_: &T, retval: &mut Zval| -> PhpResult {
200+
let value = get(self_);
201+
value
202+
.set_zval(retval, false)
203+
.map_err(|e| format!("Failed to return property value to PHP: {e:?}"))?;
204+
Ok(())
205+
}) as Box<dyn Fn(&T, &mut Zval) -> PhpResult + Send + Sync + 'a>;
206+
207+
Self::Method {
208+
get: Some(get),
209+
set: None,
210+
}
211+
}
212+
213+
/// Creates a setter-only method property.
214+
///
215+
/// This is useful when the setter accepts a type that only implements
216+
/// [`FromZval`] but not [`IntoZval`].
217+
///
218+
/// # Parameters
219+
///
220+
/// * `set` - Function used to set the value of the property.
221+
///
222+
/// # Examples
223+
///
224+
/// ```no_run
225+
/// # use ext_php_rs::props::Property;
226+
/// struct Test {
227+
/// value: String,
228+
/// }
229+
///
230+
/// impl Test {
231+
/// pub fn set_prop(&mut self, val: String) {
232+
/// self.value = val;
233+
/// }
234+
/// }
235+
///
236+
/// let prop: Property<Test> = Property::method_setter(Test::set_prop);
237+
/// ```
238+
#[must_use]
239+
pub fn method_setter<V>(set: fn(&mut T, V)) -> Self
240+
where
241+
for<'b> V: FromZval<'b> + 'a,
242+
{
243+
let set = Box::new(move |self_: &mut T, value: &Zval| -> PhpResult {
244+
let val = V::from_zval(value)
245+
.ok_or("Unable to convert property value into required type.")?;
246+
set(self_, val);
247+
Ok(())
248+
}) as Box<dyn Fn(&mut T, &Zval) -> PhpResult + Send + Sync + 'a>;
249+
250+
Self::Method {
251+
get: None,
252+
set: Some(set),
253+
}
254+
}
255+
256+
/// Adds a setter to an existing getter-only property, or combines with
257+
/// another property.
258+
///
259+
/// # Parameters
260+
///
261+
/// * `other` - Another property to combine with this one. If `other` has a
262+
/// getter and this property doesn't, the getter from `other` is used. If
263+
/// `other` has a setter and this property doesn't, the setter from
264+
/// `other` is used.
265+
///
266+
/// # Returns
267+
///
268+
/// A new property with the combined getters and setters.
269+
#[must_use]
270+
pub fn combine(self, other: Self) -> Self {
271+
match (self, other) {
272+
(Property::Method { get: g1, set: s1 }, Property::Method { get: g2, set: s2 }) => {
273+
Property::Method {
274+
get: g1.or(g2),
275+
set: s1.or(s2),
276+
}
277+
}
278+
// If either is a field property, prefer the method property
279+
(Property::Field(_), other @ Property::Method { .. }) => other,
280+
// If first is method or both are fields, prefer the first one
281+
(this @ (Property::Method { .. } | Property::Field(_)), Property::Field(_)) => this,
282+
}
283+
}
284+
170285
/// Attempts to retrieve the value of the property from the given object
171286
/// `self_`.
172287
///

tests/sapi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ fn test_sapi_multithread() {
166166
Ok(zval) => {
167167
assert!(zval.is_string());
168168
let string = zval.string().unwrap();
169-
let output = string.to_string();
169+
let output = string.clone();
170170
assert_eq!(output, format!("Hello, thread-{i}!"));
171171

172172
results.lock().unwrap().push((i, output));

tests/src/integration/class/class.php

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@
66
$class = test_class('lorem ipsum', 2022);
77
assert($class instanceof TestClass);
88

9-
// Tests getter/setter
10-
assert($class->getString() === 'lorem ipsum');
11-
$class->setString('dolor et');
12-
assert($class->getString() === 'dolor et');
9+
// Tests getter/setter as properties (get_string -> $class->string property)
10+
assert($class->string === 'lorem ipsum');
11+
$class->string = 'dolor et';
12+
assert($class->string === 'dolor et');
1313
$class->selfRef("foo");
14-
assert($class->getString() === 'Changed to foo');
14+
assert($class->string === 'Changed to foo');
1515
$class->selfMultiRef("bar");
16-
assert($class->getString() === 'Changed to bar');
16+
assert($class->string === 'Changed to bar');
1717

18-
assert($class->getNumber() === 2022);
19-
$class->setNumber(2023);
20-
assert($class->getNumber() === 2023);
18+
assert($class->number === 2022);
19+
$class->number = 2023;
20+
assert($class->number === 2023);
2121

2222
var_dump($class);
2323
// Tests #prop decorator
@@ -51,3 +51,7 @@
5151

5252
$classReflection = new ReflectionClass(TestClassProtectedConstruct::class);
5353
assert($classReflection->getMethod('__construct')->isProtected());
54+
55+
// Test issue #325 - returning &'static str from getter
56+
$staticStrClass = new TestClassStaticStrGetter();
57+
assert($staticStrClass->static_value === 'Hello from static str');

tests/src/integration/class/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,24 @@ impl TestClassProtectedConstruct {
175175
}
176176
}
177177

178+
/// Test class for issue #325 - returning &'static str from getter
179+
#[php_class]
180+
pub struct TestClassStaticStrGetter;
181+
182+
#[php_impl]
183+
impl TestClassStaticStrGetter {
184+
pub fn __construct() -> Self {
185+
Self
186+
}
187+
188+
/// This getter returns a &'static str which previously failed to compile
189+
/// due to "implementation of `FromZval` is not general enough" error.
190+
#[php(getter)]
191+
pub fn get_static_value(&self) -> &'static str {
192+
"Hello from static str"
193+
}
194+
}
195+
178196
pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
179197
builder
180198
.class::<TestClass>()
@@ -183,6 +201,7 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
183201
.class::<TestClassExtendsImpl>()
184202
.class::<TestClassMethodVisibility>()
185203
.class::<TestClassProtectedConstruct>()
204+
.class::<TestClassStaticStrGetter>()
186205
.function(wrap_function!(test_class))
187206
.function(wrap_function!(throw_exception))
188207
}

0 commit comments

Comments
 (0)