ext_php_rs/
convert.rs

1//! Traits used to convert between Zend/PHP and Rust types.
2
3use crate::{
4    boxed::ZBox,
5    error::Result,
6    exception::PhpException,
7    flags::DataType,
8    types::{ZendObject, Zval},
9};
10
11/// Allows zvals to be converted into Rust types in a fallible way. Reciprocal
12/// of the [`IntoZval`] trait.
13pub trait FromZval<'a>: Sized {
14    /// The corresponding type of the implemented value in PHP.
15    const TYPE: DataType;
16
17    /// Attempts to retrieve an instance of `Self` from a reference to a
18    /// [`Zval`].
19    ///
20    /// # Parameters
21    ///
22    /// * `zval` - Zval to get value from.
23    fn from_zval(zval: &'a Zval) -> Option<Self>;
24}
25
26impl<'a, T> FromZval<'a> for Option<T>
27where
28    T: FromZval<'a>,
29{
30    const TYPE: DataType = T::TYPE;
31
32    fn from_zval(zval: &'a Zval) -> Option<Self> {
33        Some(T::from_zval(zval))
34    }
35}
36
37/// Allows mutable zvals to be converted into Rust types in a fallible way.
38///
39/// If `Self` does not require the zval to be mutable to be extracted, you
40/// should implement [`FromZval`] instead, as this trait is generically
41/// implemented for any type that implements [`FromZval`].
42pub trait FromZvalMut<'a>: Sized {
43    /// The corresponding type of the implemented value in PHP.
44    const TYPE: DataType;
45
46    /// Attempts to retrieve an instance of `Self` from a mutable reference to a
47    /// [`Zval`].
48    ///
49    /// # Parameters
50    ///
51    /// * `zval` - Zval to get value from.
52    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self>;
53}
54
55impl<'a, T> FromZvalMut<'a> for T
56where
57    T: FromZval<'a>,
58{
59    const TYPE: DataType = <T as FromZval>::TYPE;
60
61    #[inline]
62    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
63        Self::from_zval(zval)
64    }
65}
66
67/// `FromZendObject` is implemented by types which can be extracted from a Zend
68/// object.
69///
70/// Normal usage is through the helper method `ZendObject::extract`:
71///
72/// ```rust,ignore
73/// let obj: ZendObject = ...;
74/// let repr: String = obj.extract();
75/// let props: HashMap = obj.extract();
76/// ```
77///
78/// Should be functionally equivalent to casting an object to another compatible
79/// type.
80pub trait FromZendObject<'a>: Sized {
81    /// Extracts `Self` from the source `ZendObject`.
82    ///
83    /// # Errors
84    ///
85    /// If the conversion fails, an [`Error`] is returned.
86    ///
87    /// [`Error`]: crate::error::Error
88    // TODO: Expand on error information
89    fn from_zend_object(obj: &'a ZendObject) -> Result<Self>;
90}
91
92/// Implemented on types which can be extracted from a mutable zend object.
93///
94/// If `Self` does not require the object to be mutable, it should implement
95/// [`FromZendObject`] instead, as this trait is generically implemented for
96/// any types that also implement [`FromZendObject`].
97pub trait FromZendObjectMut<'a>: Sized {
98    /// Extracts `Self` from the source `ZendObject`.
99    ///
100    /// # Errors
101    ///
102    /// If the conversion fails, an [`Error`] is returned.
103    ///
104    /// [`Error`]: crate::error::Error
105    // TODO: Expand on error information
106    fn from_zend_object_mut(obj: &'a mut ZendObject) -> Result<Self>;
107}
108
109impl<'a, T> FromZendObjectMut<'a> for T
110where
111    T: FromZendObject<'a>,
112{
113    #[inline]
114    fn from_zend_object_mut(obj: &'a mut ZendObject) -> Result<Self> {
115        Self::from_zend_object(obj)
116    }
117}
118
119/// Implemented on types which can be converted into a Zend object. It is up to
120/// the implementation to determine the type of object which is produced.
121pub trait IntoZendObject {
122    /// Attempts to convert `self` into a Zend object.
123    ///
124    /// # Errors
125    ///
126    /// If the conversion fails, an [`Error`] is returned.
127    ///
128    /// [`Error`]: crate::error::Error
129    // TODO: Expand on error information
130    fn into_zend_object(self) -> Result<ZBox<ZendObject>>;
131}
132
133/// Provides implementations for converting Rust primitive types into PHP zvals.
134/// Alternative to the built-in Rust [`From`] and [`TryFrom`] implementations,
135/// allowing the caller to specify whether the Zval contents will persist
136/// between requests.
137///
138/// [`TryFrom`]: std::convert::TryFrom
139pub trait IntoZval: Sized {
140    /// The corresponding type of the implemented value in PHP.
141    const TYPE: DataType;
142
143    /// Whether converting into a [`Zval`] may result in null.
144    const NULLABLE: bool;
145
146    /// Converts a Rust primitive type into a Zval. Returns a result containing
147    /// the Zval if successful.
148    ///
149    /// # Parameters
150    ///
151    /// * `persistent` - Whether the contents of the Zval will persist between
152    ///   requests.
153    ///
154    /// # Errors
155    ///
156    /// If the conversion fails, an [`Error`] is returned.
157    ///
158    /// [`Error`]: crate::error::Error
159    // TODO: Expand on error information
160    fn into_zval(self, persistent: bool) -> Result<Zval> {
161        let mut zval = Zval::new();
162        self.set_zval(&mut zval, persistent)?;
163        Ok(zval)
164    }
165
166    /// Sets the content of a pre-existing zval. Returns a result containing
167    /// nothing if setting the content was successful.
168    ///
169    /// # Parameters
170    ///
171    /// * `zv` - The Zval to set the content of.
172    /// * `persistent` - Whether the contents of the Zval will persist between
173    ///   requests.
174    ///
175    /// # Errors
176    ///
177    /// If setting the content fails, an [`Error`] is returned.
178    ///
179    /// [`Error`]: crate::error::Error
180    // TODO: Expand on error information
181    fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()>;
182}
183
184impl IntoZval for () {
185    const TYPE: DataType = DataType::Void;
186    const NULLABLE: bool = true;
187
188    #[inline]
189    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
190        zv.set_null();
191        Ok(())
192    }
193}
194
195impl<T> IntoZval for Option<T>
196where
197    T: IntoZval,
198{
199    const TYPE: DataType = T::TYPE;
200    const NULLABLE: bool = true;
201
202    #[inline]
203    fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()> {
204        if let Some(val) = self {
205            val.set_zval(zv, persistent)
206        } else {
207            zv.set_null();
208            Ok(())
209        }
210    }
211}
212
213impl<T, E> IntoZval for std::result::Result<T, E>
214where
215    T: IntoZval,
216    E: Into<PhpException>,
217{
218    const TYPE: DataType = T::TYPE;
219    const NULLABLE: bool = T::NULLABLE;
220
221    fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()> {
222        match self {
223            Ok(val) => val.set_zval(zv, persistent),
224            Err(e) => {
225                let ex: PhpException = e.into();
226                ex.throw()
227            }
228        }
229    }
230}
231
232/// An object-safe version of the [`IntoZval`] trait.
233///
234/// This trait is automatically implemented on any type that implements both
235/// [`IntoZval`] and [`Clone`]. You avoid implementing this trait directly,
236/// rather implement these two other traits.
237pub trait IntoZvalDyn {
238    /// Converts a Rust primitive type into a Zval. Returns a result containing
239    /// the Zval if successful. `self` is cloned before being converted into
240    /// a zval.
241    ///
242    /// # Parameters
243    ///
244    /// * `persistent` - Whether the contents of the Zval will persist between
245    ///   requests.
246    ///
247    /// # Errors
248    ///
249    /// If the conversion fails, an [`Error`] is returned.
250    ///
251    /// [`Error`]: crate::error::Error
252    fn as_zval(&self, persistent: bool) -> Result<Zval>;
253
254    /// Returns the PHP type of the type.
255    fn get_type(&self) -> DataType;
256
257    /// Returns the PHP stub representation of this value.
258    ///
259    /// This is used when generating PHP stub files for IDE autocompletion.
260    /// The returned string should be a valid PHP literal.
261    fn stub_value(&self) -> String {
262        // Default implementation - convert to zval and format
263        match self.as_zval(false) {
264            Ok(zval) => zval_to_stub(&zval),
265            Err(_) => "null".to_string(),
266        }
267    }
268}
269
270/// Converts a Zval to its PHP stub representation.
271#[must_use]
272#[allow(clippy::match_same_arms)]
273pub fn zval_to_stub(zval: &Zval) -> String {
274    use crate::flags::DataType;
275
276    match zval.get_type() {
277        DataType::Null | DataType::Undef => "null".to_string(),
278        DataType::True => "true".to_string(),
279        DataType::False => "false".to_string(),
280        DataType::Long => zval
281            .long()
282            .map_or_else(|| "null".to_string(), |v| v.to_string()),
283        DataType::Double => zval
284            .double()
285            .map_or_else(|| "null".to_string(), |v| v.to_string()),
286        DataType::String => {
287            if let Some(s) = zval.str() {
288                let escaped = s
289                    .replace('\\', "\\\\")
290                    .replace('\'', "\\'")
291                    .replace('\n', "\\n")
292                    .replace('\r', "\\r")
293                    .replace('\t', "\\t");
294                format!("'{escaped}'")
295            } else {
296                "null".to_string()
297            }
298        }
299        DataType::Array => {
300            #[allow(clippy::explicit_iter_loop)]
301            if let Some(arr) = zval.array() {
302                // Check if array has sequential numeric keys starting from 0
303                let is_sequential = arr.iter().enumerate().all(|(i, (key, _))| {
304                    matches!(key, crate::types::ArrayKey::Long(idx) if i64::try_from(i).is_ok_and(|ii| idx == ii))
305                });
306
307                let mut parts = Vec::new();
308                for (key, val) in arr.iter() {
309                    let val_str = zval_to_stub(val);
310                    if is_sequential {
311                        parts.push(val_str);
312                    } else {
313                        match key {
314                            crate::types::ArrayKey::Long(idx) => {
315                                parts.push(format!("{idx} => {val_str}"));
316                            }
317                            crate::types::ArrayKey::String(key) => {
318                                let key_escaped = key.replace('\\', "\\\\").replace('\'', "\\'");
319                                parts.push(format!("'{key_escaped}' => {val_str}"));
320                            }
321                            crate::types::ArrayKey::Str(key) => {
322                                let key_escaped = key.replace('\\', "\\\\").replace('\'', "\\'");
323                                parts.push(format!("'{key_escaped}' => {val_str}"));
324                            }
325                        }
326                    }
327                }
328                format!("[{}]", parts.join(", "))
329            } else {
330                "[]".to_string()
331            }
332        }
333        _ => "null".to_string(),
334    }
335}
336
337impl<T: IntoZval + Clone> IntoZvalDyn for T {
338    fn as_zval(&self, persistent: bool) -> Result<Zval> {
339        self.clone().into_zval(persistent)
340    }
341
342    fn get_type(&self) -> DataType {
343        Self::TYPE
344    }
345}
346
347impl IntoZvalDyn for Zval {
348    fn as_zval(&self, _persistent: bool) -> Result<Zval> {
349        Ok(self.shallow_clone())
350    }
351
352    fn get_type(&self) -> DataType {
353        self.get_type()
354    }
355}