Skip to main content

ext_php_rs/
props.rs

1//! Types and traits for adding properties to PHP classes registered from Rust.
2//!
3//! There are two types of properties:
4//!
5//! * Field properties, referencing a property on a struct.
6//! * Method properties, a getter and/or setter function called to get/set the
7//!   value of the property.
8//!
9//! Field types which can be used as a property implement [`Prop`]. This is
10//! automatically implemented on any type which implements [`Clone`],
11//! [`IntoZval`] and [`FromZval`].
12//!
13//! Method property types only have to implement [`IntoZval`] for setters and
14//! [`FromZval`] for getters.
15//!
16//! Properties are stored in the [`Property`] type, which allows us to store
17//! field and method properties in one data structure. Properties are usually
18//! retrieved via the [`RegisteredClass`] trait.
19//!
20//! [`RegisteredClass`]: crate::class::RegisteredClass
21
22use crate::{
23    convert::{FromZval, IntoZval},
24    error::{Error, Result},
25    exception::PhpResult,
26    types::Zval,
27};
28
29/// Implemented on types which can be used as PHP properties.
30///
31/// Generally, this should not be directly implemented on types, as it is
32/// automatically implemented on types that implement [`Clone`], [`IntoZval`]
33/// and [`FromZval`], which will be required to implement this trait regardless.
34pub trait Prop<'a> {
35    /// Gets the value of `self` by setting the value of `zv`.
36    ///
37    /// # Parameters
38    ///
39    /// * `zv` - The zval to set the value of.
40    ///
41    /// # Errors
42    ///
43    /// If the property could not be read.
44    fn get(&self, zv: &mut Zval) -> Result<()>;
45
46    /// Sets the value of `self` with the contents of a given zval `zv`.
47    ///
48    /// # Parameters
49    ///
50    /// * `zv` - The zval containing the new value of `self`.
51    ///
52    /// # Errors
53    ///
54    /// If the property could not be set
55    fn set(&mut self, zv: &'a Zval) -> Result<()>;
56}
57
58impl<'a, T: Clone + IntoZval + FromZval<'a>> Prop<'a> for T {
59    fn get(&self, zv: &mut Zval) -> Result<()> {
60        self.clone().set_zval(zv, false)
61    }
62
63    fn set(&mut self, zv: &'a Zval) -> Result<()> {
64        let x = Self::from_zval(zv).ok_or_else(|| Error::ZvalConversion(zv.get_type()))?;
65        *self = x;
66        Ok(())
67    }
68}
69
70/// A getter for a property
71pub type PropertyGetter<'a, T> = Option<Box<dyn Fn(&T, &mut Zval) -> PhpResult + Send + Sync + 'a>>;
72/// A setter for a property
73pub type PropertySetter<'a, T> = Option<Box<dyn Fn(&mut T, &Zval) -> PhpResult + Send + Sync + 'a>>;
74
75/// Represents a property added to a PHP class.
76pub enum Property<'a, T> {
77    /// Field properties, where the data is stored inside a struct field.
78    Field(Box<dyn (Fn(&mut T) -> &mut dyn Prop) + Send + Sync>),
79    /// Method properties, where getter and/or setter functions are provided,
80    /// which are used to get and set the value of the property.
81    Method {
82        /// Getter function for the property.
83        get: PropertyGetter<'a, T>,
84        /// Setter function for the property.
85        set: PropertySetter<'a, T>,
86    },
87}
88
89impl<'a, T: 'a> Property<'a, T> {
90    /// Creates a field property.
91    ///
92    /// # Parameters
93    ///
94    /// * `f` - The function used to get a mutable reference to the property.
95    ///
96    /// # Examples
97    ///
98    /// ```no_run
99    /// # use ext_php_rs::props::Property;
100    /// struct Test {
101    ///     pub a: i32,
102    /// }
103    ///
104    /// let prop: Property<Test> = Property::field(|test: &mut Test| &mut test.a);
105    /// ```
106    pub fn field<F>(f: F) -> Self
107    where
108        F: (Fn(&mut T) -> &mut dyn Prop) + Send + Sync + 'static,
109    {
110        Self::Field(Box::new(f) as Box<dyn (Fn(&mut T) -> &mut dyn Prop) + Send + Sync>)
111    }
112
113    /// Creates a method property with getters and setters.
114    ///
115    /// If either the getter or setter is not given, an exception will be thrown
116    /// when attempting to retrieve/set the property.
117    ///
118    /// # Parameters
119    ///
120    /// * `get` - Function used to get the value of the property, in an
121    ///   [`Option`].
122    /// * `set` - Function used to set the value of the property, in an
123    ///   [`Option`].
124    ///
125    /// # Examples
126    ///
127    /// ```no_run
128    /// # use ext_php_rs::props::Property;
129    /// struct Test;
130    ///
131    /// impl Test {
132    ///     pub fn get_prop(&self) -> String {
133    ///         "Hello".into()
134    ///     }
135    ///
136    ///     pub fn set_prop(&mut self, val: String) {
137    ///         println!("{}", val);
138    ///     }
139    /// }
140    ///
141    /// let prop: Property<Test> = Property::method(Some(Test::get_prop), Some(Test::set_prop));
142    /// ```
143    #[must_use]
144    pub fn method<V>(get: Option<fn(&T) -> V>, set: Option<fn(&mut T, V)>) -> Self
145    where
146        for<'b> V: IntoZval + FromZval<'b> + 'a,
147    {
148        let get = get.map(|get| {
149            Box::new(move |self_: &T, retval: &mut Zval| -> PhpResult {
150                let value = get(self_);
151                value
152                    .set_zval(retval, false)
153                    .map_err(|e| format!("Failed to return property value to PHP: {e:?}"))?;
154                Ok(())
155            }) as Box<dyn Fn(&T, &mut Zval) -> PhpResult + Send + Sync + 'a>
156        });
157
158        let set = set.map(|set| {
159            Box::new(move |self_: &mut T, value: &Zval| -> PhpResult {
160                let val = V::from_zval(value)
161                    .ok_or("Unable to convert property value into required type.")?;
162                set(self_, val);
163                Ok(())
164            }) as Box<dyn Fn(&mut T, &Zval) -> PhpResult + Send + Sync + 'a>
165        });
166
167        Self::Method { get, set }
168    }
169
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
285    /// Attempts to retrieve the value of the property from the given object
286    /// `self_`.
287    ///
288    /// The value of the property, if successfully retrieved, is loaded into the
289    /// given [`Zval`] `retval`. If unsuccessful, a [`PhpException`] is
290    /// returned inside the error variant of a result.
291    ///
292    /// # Parameters
293    ///
294    /// * `self_` - The object to retrieve the property from.
295    /// * `retval` - The [`Zval`] to load the value of the property into.
296    ///
297    /// # Returns
298    ///
299    /// Nothing upon success
300    ///
301    /// # Errors
302    ///
303    /// A [`PhpException`] inside an error variant when the property could not
304    /// be retrieved.
305    ///
306    /// # Examples
307    ///
308    /// ```no_run
309    /// # use ext_php_rs::props::Property;
310    /// # use ext_php_rs::types::Zval;
311    /// struct Test {
312    ///     pub a: i32,
313    /// }
314    ///
315    /// let prop: Property<Test> = Property::field(|obj: &mut Test| &mut obj.a);
316    ///
317    /// let mut test = Test { a: 500 };
318    /// let mut zv = Zval::new();
319    /// prop.get(&mut test, &mut zv).unwrap();
320    /// assert_eq!(zv.long(), Some(500));
321    /// ```
322    ///
323    /// [`PhpException`]: crate::exception::PhpException
324    pub fn get(&self, self_: &'a mut T, retval: &mut Zval) -> PhpResult {
325        match self {
326            Property::Field(field) => field(self_)
327                .get(retval)
328                .map_err(|e| format!("Failed to get property value: {e:?}").into()),
329            Property::Method { get, set: _ } => match get {
330                Some(get) => get(self_, retval),
331                None => Err("No getter available for this property.".into()),
332            },
333        }
334    }
335
336    /// Attempts to set the value of the property inside the given object
337    /// `self_`.
338    ///
339    /// The new value of the property is supplied inside the given [`Zval`]
340    /// `value`. If unsuccessful, a [`PhpException`] is returned inside the
341    /// error variant of a result.
342    ///
343    /// # Parameters
344    ///
345    /// * `self_` - The object to set the property in.
346    /// * `value` - The [`Zval`] containing the new content for the property.
347    ///
348    /// # Returns
349    ///
350    /// Nothing upon success
351    ///
352    /// # Errors
353    ///
354    /// A [`PhpException`] inside an error variant when the property could not
355    /// be set.
356    ///
357    /// # Examples
358    ///
359    /// ```no_run
360    /// # use ext_php_rs::props::Property;
361    /// # use ext_php_rs::types::Zval;
362    /// # use ext_php_rs::convert::IntoZval;
363    /// struct Test {
364    ///     pub a: i32,
365    /// }
366    ///
367    /// let prop: Property<Test> = Property::field(|obj: &mut Test| &mut obj.a);
368    ///
369    /// let mut test = Test { a: 500 };
370    /// let zv = 100.into_zval(false).unwrap();
371    /// prop.set(&mut test, &zv).unwrap();
372    /// assert_eq!(test.a, 100);
373    /// ```
374    ///
375    /// [`PhpException`]: crate::exception::PhpException
376    pub fn set(&self, self_: &'a mut T, value: &Zval) -> PhpResult {
377        match self {
378            Property::Field(field) => field(self_)
379                .set(value)
380                .map_err(|e| format!("Failed to set property value: {e:?}").into()),
381            Property::Method { get: _, set } => match set {
382                Some(set) => set(self_, value),
383                None => Err("No setter available for this property.".into()),
384            },
385        }
386    }
387}