Skip to main content

ext_php_rs/
convert.rs

1//! Traits used to convert between Zend/PHP and Rust types.
2
3use crate::{
4    boxed::ZBox,
5    error::{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.
13#[diagnostic::on_unimplemented(
14    message = "`{Self}` cannot be extracted from a PHP value",
15    label = "no `FromZval` implementation for `{Self}`"
16)]
17pub trait FromZval<'a>: Sized {
18    /// The corresponding type of the implemented value in PHP.
19    const TYPE: DataType;
20
21    /// Whether a PHP argument of this type accepts `null` and may be omitted.
22    ///
23    /// Drives the `?T` nullability bit and the optional position of a
24    /// parameter declared with this type.
25    const NULLABLE: bool = false;
26
27    /// Attempts to retrieve an instance of `Self` from a reference to a
28    /// [`Zval`].
29    ///
30    /// # Parameters
31    ///
32    /// * `zval` - Zval to get value from.
33    fn from_zval(zval: &'a Zval) -> Option<Self>;
34
35    /// The value to use when a PHP argument of this type was omitted by the
36    /// caller. `None` means the argument is required.
37    ///
38    /// It must return `Some` exactly when `NULLABLE` is `true`. The macros
39    /// decide from `NULLABLE` alone which parameters PHP callers may omit.
40    #[must_use]
41    fn from_missing() -> Option<Self> {
42        None
43    }
44}
45
46impl<'a, T> FromZval<'a> for Option<T>
47where
48    T: FromZval<'a>,
49{
50    const TYPE: DataType = T::TYPE;
51    const NULLABLE: bool = true;
52
53    fn from_zval(zval: &'a Zval) -> Option<Self> {
54        Some(T::from_zval(zval))
55    }
56
57    fn from_missing() -> Option<Self> {
58        Some(None)
59    }
60}
61
62/// Allows mutable zvals to be converted into Rust types in a fallible way.
63///
64/// If `Self` does not require the zval to be mutable to be extracted, you
65/// should implement [`FromZval`] instead, as this trait is generically
66/// implemented for any type that implements [`FromZval`].
67#[diagnostic::on_unimplemented(
68    message = "`{Self}` cannot be used as a PHP function argument",
69    label = "no `FromZval` or `FromZvalMut` implementation for `{Self}`",
70    note = "`Option<&mut T>` is not supported for object arguments, use `&mut T`: PHP objects are handles and the caller sees every mutation",
71    note = "a variadic parameter must be spelled `&[T]` in the signature, a type alias hiding the slice is not detected"
72)]
73pub trait FromZvalMut<'a>: Sized {
74    /// The corresponding type of the implemented value in PHP.
75    const TYPE: DataType;
76
77    /// Whether a PHP argument of this type accepts `null` and may be omitted.
78    const NULLABLE: bool = false;
79
80    /// Whether a PHP argument of this type is declared pass-by-reference
81    /// (`&$arg`). Set it for types that mutate a PHP value in place, so the
82    /// engine separates the value before the call.
83    const BY_REF: bool = false;
84
85    /// Attempts to retrieve an instance of `Self` from a mutable reference to a
86    /// [`Zval`].
87    ///
88    /// # Parameters
89    ///
90    /// * `zval` - Zval to get value from.
91    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self>;
92
93    /// The value to use when a PHP argument of this type was omitted by the
94    /// caller. `None` means the argument is required.
95    ///
96    /// It must return `Some` exactly when `NULLABLE` is `true`. The macros
97    /// decide from `NULLABLE` alone which parameters PHP callers may omit.
98    #[must_use]
99    fn from_missing() -> Option<Self> {
100        None
101    }
102}
103
104impl<'a, T> FromZvalMut<'a> for T
105where
106    T: FromZval<'a>,
107{
108    const TYPE: DataType = <T as FromZval>::TYPE;
109    const NULLABLE: bool = <T as FromZval>::NULLABLE;
110
111    #[inline]
112    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
113        Self::from_zval(zval)
114    }
115
116    #[inline]
117    fn from_missing() -> Option<Self> {
118        <T as FromZval>::from_missing()
119    }
120}
121
122/// `FromZendObject` is implemented by types which can be extracted from a Zend
123/// object.
124///
125/// Normal usage is through the helper method `ZendObject::extract`:
126///
127/// ```rust,ignore
128/// let obj: ZendObject = ...;
129/// let repr: String = obj.extract();
130/// let props: HashMap = obj.extract();
131/// ```
132///
133/// Should be functionally equivalent to casting an object to another compatible
134/// type.
135pub trait FromZendObject<'a>: Sized {
136    /// Extracts `Self` from the source `ZendObject`.
137    ///
138    /// # Errors
139    ///
140    /// * [`Error::ZvalConversion`] - If the object is not of the expected
141    ///   class.
142    /// * [`Error::InvalidProperty`] - If a property the conversion needs is
143    ///   missing.
144    ///
145    /// [`Error::ZvalConversion`]: crate::error::Error::ZvalConversion
146    /// [`Error::InvalidProperty`]: crate::error::Error::InvalidProperty
147    fn from_zend_object(obj: &'a ZendObject) -> Result<Self>;
148}
149
150/// Implemented on types which can be extracted from a mutable zend object.
151///
152/// If `Self` does not require the object to be mutable, it should implement
153/// [`FromZendObject`] instead, as this trait is generically implemented for
154/// any types that also implement [`FromZendObject`].
155pub trait FromZendObjectMut<'a>: Sized {
156    /// Extracts `Self` from the source `ZendObject`.
157    ///
158    /// # Errors
159    ///
160    /// * [`Error::ZvalConversion`] - If the object is not of the expected
161    ///   class.
162    /// * [`Error::InvalidProperty`] - If a property the conversion needs is
163    ///   missing.
164    ///
165    /// [`Error::ZvalConversion`]: crate::error::Error::ZvalConversion
166    /// [`Error::InvalidProperty`]: crate::error::Error::InvalidProperty
167    fn from_zend_object_mut(obj: &'a mut ZendObject) -> Result<Self>;
168}
169
170impl<'a, T> FromZendObjectMut<'a> for T
171where
172    T: FromZendObject<'a>,
173{
174    #[inline]
175    fn from_zend_object_mut(obj: &'a mut ZendObject) -> Result<Self> {
176        Self::from_zend_object(obj)
177    }
178}
179
180/// Implemented on types which can be converted into a Zend object. It is up to
181/// the implementation to determine the type of object which is produced.
182pub trait IntoZendObject {
183    /// Attempts to convert `self` into a Zend object.
184    ///
185    /// # Errors
186    ///
187    /// * [`Error::ZvalConversion`] - If the value has no object
188    ///   representation.
189    /// * [`Error::InvalidScope`] - If the class of the object has not been
190    ///   registered with the engine.
191    ///
192    /// [`Error::ZvalConversion`]: crate::error::Error::ZvalConversion
193    /// [`Error::InvalidScope`]: crate::error::Error::InvalidScope
194    fn into_zend_object(self) -> Result<ZBox<ZendObject>>;
195}
196
197/// Provides implementations for converting Rust primitive types into PHP zvals.
198/// Alternative to the built-in Rust [`From`] and [`TryFrom`] implementations,
199/// allowing the caller to specify whether the Zval contents will persist
200/// between requests.
201///
202/// [`TryFrom`]: std::convert::TryFrom
203pub trait IntoZval: Sized {
204    /// The corresponding type of the implemented value in PHP.
205    const TYPE: DataType;
206
207    /// Whether converting into a [`Zval`] may result in null.
208    const NULLABLE: bool;
209
210    /// Converts a Rust primitive type into a Zval. Returns a result containing
211    /// the Zval if successful.
212    ///
213    /// # Parameters
214    ///
215    /// * `persistent` - Whether the contents of the Zval will persist between
216    ///   requests.
217    ///
218    /// # Errors
219    ///
220    /// Implementations return the error that describes their failure, for
221    /// example:
222    ///
223    /// * [`Error::IntegerOverflow`] - If the value does not fit in a PHP
224    ///   integer.
225    /// * [`Error::InvalidCString`] - If a string in the value contains a NUL
226    ///   byte.
227    ///
228    /// [`Error::IntegerOverflow`]: crate::error::Error::IntegerOverflow
229    /// [`Error::InvalidCString`]: crate::error::Error::InvalidCString
230    fn into_zval(self, persistent: bool) -> Result<Zval> {
231        let mut zval = Zval::new();
232        self.set_zval(&mut zval, persistent)?;
233        Ok(zval)
234    }
235
236    /// Sets the content of a pre-existing zval. Returns a result containing
237    /// nothing if setting the content was successful.
238    ///
239    /// # Parameters
240    ///
241    /// * `zv` - The Zval to set the content of.
242    /// * `persistent` - Whether the contents of the Zval will persist between
243    ///   requests.
244    ///
245    /// # Errors
246    ///
247    /// Implementations return the error that describes their failure, for
248    /// example:
249    ///
250    /// * [`Error::IntegerOverflow`] - If the value does not fit in a PHP
251    ///   integer.
252    /// * [`Error::InvalidCString`] - If a string in the value contains a NUL
253    ///   byte.
254    ///
255    /// [`Error::IntegerOverflow`]: crate::error::Error::IntegerOverflow
256    /// [`Error::InvalidCString`]: crate::error::Error::InvalidCString
257    fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()>;
258}
259
260impl IntoZval for () {
261    const TYPE: DataType = DataType::Void;
262    const NULLABLE: bool = true;
263
264    #[inline]
265    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
266        zv.set_null();
267        Ok(())
268    }
269}
270
271impl<T> IntoZval for Option<T>
272where
273    T: IntoZval,
274{
275    const TYPE: DataType = T::TYPE;
276    const NULLABLE: bool = true;
277
278    #[inline]
279    fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()> {
280        if let Some(val) = self {
281            val.set_zval(zv, persistent)
282        } else {
283            zv.set_null();
284            Ok(())
285        }
286    }
287}
288
289impl<T, E> IntoZval for std::result::Result<T, E>
290where
291    T: IntoZval,
292    E: Into<PhpException>,
293{
294    const TYPE: DataType = T::TYPE;
295    const NULLABLE: bool = T::NULLABLE;
296
297    fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()> {
298        match self {
299            Ok(val) => val.set_zval(zv, persistent),
300            Err(e) => {
301                let ex: PhpException = e.into();
302                ex.throw();
303                Err(Error::pending_exception().unwrap_or(Error::Callable))
304            }
305        }
306    }
307}
308
309/// An object-safe version of the [`IntoZval`] trait.
310///
311/// This trait is automatically implemented on any type that implements both
312/// [`IntoZval`] and [`Clone`]. You avoid implementing this trait directly,
313/// rather implement these two other traits.
314pub trait IntoZvalDyn {
315    /// Converts a Rust primitive type into a Zval. Returns a result containing
316    /// the Zval if successful. `self` is cloned before being converted into
317    /// a zval.
318    ///
319    /// # Parameters
320    ///
321    /// * `persistent` - Whether the contents of the Zval will persist between
322    ///   requests.
323    ///
324    /// # Errors
325    ///
326    /// If the conversion fails, an [`Error`] is returned.
327    ///
328    /// [`Error`]: crate::error::Error
329    fn as_zval(&self, persistent: bool) -> Result<Zval>;
330
331    /// Returns the PHP type of the type.
332    fn get_type(&self) -> DataType;
333
334    /// Returns the PHP stub representation of this value.
335    ///
336    /// This is used when generating PHP stub files for IDE autocompletion.
337    /// The returned string should be a valid PHP literal.
338    fn stub_value(&self) -> String {
339        // Default implementation - convert to zval and format
340        match self.as_zval(false) {
341            Ok(zval) => zval_to_stub(&zval),
342            Err(_) => "null".to_string(),
343        }
344    }
345}
346
347/// Converts a Zval to its PHP stub representation.
348#[must_use]
349#[allow(clippy::match_same_arms)]
350pub fn zval_to_stub(zval: &Zval) -> String {
351    use crate::flags::DataType;
352
353    match zval.get_type() {
354        DataType::Null | DataType::Undef => "null".to_string(),
355        DataType::True => "true".to_string(),
356        DataType::False => "false".to_string(),
357        DataType::Long => zval
358            .long()
359            .map_or_else(|| "null".to_string(), |v| v.to_string()),
360        DataType::Double => zval
361            .double()
362            .map_or_else(|| "null".to_string(), |v| v.to_string()),
363        DataType::String => {
364            if let Some(s) = zval.str() {
365                let escaped = s
366                    .replace('\\', "\\\\")
367                    .replace('\'', "\\'")
368                    .replace('\n', "\\n")
369                    .replace('\r', "\\r")
370                    .replace('\t', "\\t");
371                format!("'{escaped}'")
372            } else {
373                "null".to_string()
374            }
375        }
376        DataType::Array => {
377            #[allow(clippy::explicit_iter_loop)]
378            if let Some(arr) = zval.array() {
379                // Check if array has sequential numeric keys starting from 0
380                let is_sequential = arr.iter().enumerate().all(|(i, (key, _))| {
381                    matches!(key, crate::types::ArrayKey::Long(idx) if i64::try_from(i).is_ok_and(|ii| idx == ii))
382                });
383
384                let mut parts = Vec::new();
385                for (key, val) in arr.iter() {
386                    let val_str = zval_to_stub(val);
387                    if is_sequential {
388                        parts.push(val_str);
389                    } else {
390                        match key {
391                            crate::types::ArrayKey::Long(idx) => {
392                                parts.push(format!("{idx} => {val_str}"));
393                            }
394                            crate::types::ArrayKey::String(key) => {
395                                let key_escaped = key.replace('\\', "\\\\").replace('\'', "\\'");
396                                parts.push(format!("'{key_escaped}' => {val_str}"));
397                            }
398                            crate::types::ArrayKey::Str(key) => {
399                                let key_escaped = key.replace('\\', "\\\\").replace('\'', "\\'");
400                                parts.push(format!("'{key_escaped}' => {val_str}"));
401                            }
402                            crate::types::ArrayKey::ZendString(key) => {
403                                let decoded = match key.as_str() {
404                                    Ok(s) => std::borrow::Cow::Borrowed(s),
405                                    Err(_) => String::from_utf8_lossy(key.as_bytes()),
406                                };
407                                let key_escaped =
408                                    decoded.replace('\\', "\\\\").replace('\'', "\\'");
409                                parts.push(format!("'{key_escaped}' => {val_str}"));
410                            }
411                        }
412                    }
413                }
414                format!("[{}]", parts.join(", "))
415            } else {
416                "[]".to_string()
417            }
418        }
419        _ => "null".to_string(),
420    }
421}
422
423impl<T: IntoZval + Clone> IntoZvalDyn for T {
424    fn as_zval(&self, persistent: bool) -> Result<Zval> {
425        self.clone().into_zval(persistent)
426    }
427
428    fn get_type(&self) -> DataType {
429        Self::TYPE
430    }
431}
432
433impl IntoZvalDyn for Zval {
434    fn as_zval(&self, _persistent: bool) -> Result<Zval> {
435        Ok(self.shallow_clone())
436    }
437
438    fn get_type(&self) -> DataType {
439        self.get_type()
440    }
441}
442
443/// Renders a Rust value as PHP source, for the `default_value` of a parameter
444/// and for stub files.
445///
446/// The rendering never touches the engine, so it can run while the module
447/// entry is built, before the Zend allocator exists. PHP reads the text back
448/// with `zend_get_default_from_internal_arg_info`, which accepts `null`,
449/// `true`, `false`, integers, quoted strings, `[]` and constant expressions.
450pub trait StubLiteral {
451    /// The PHP source text of this value.
452    fn stub_literal(&self) -> String;
453}
454
455macro_rules! stub_literal_display {
456    ($($ty:ty),*) => {
457        $(
458            impl StubLiteral for $ty {
459                fn stub_literal(&self) -> String {
460                    self.to_string()
461                }
462            }
463        )*
464    };
465}
466
467stub_literal_display!(bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
468
469macro_rules! stub_literal_float {
470    ($($ty:ty),*) => {
471        $(
472            impl StubLiteral for $ty {
473                fn stub_literal(&self) -> String {
474                    if self.is_nan() {
475                        "NAN".to_string()
476                    } else if self.is_infinite() {
477                        if *self > 0.0 { "INF" } else { "-INF" }.to_string()
478                    } else {
479                        format!("{self:?}")
480                    }
481                }
482            }
483        )*
484    };
485}
486
487stub_literal_float!(f32, f64);
488
489impl StubLiteral for str {
490    fn stub_literal(&self) -> String {
491        let escaped = self.replace('\\', "\\\\").replace('\'', "\\'");
492        format!("'{escaped}'")
493    }
494}
495
496impl StubLiteral for &str {
497    fn stub_literal(&self) -> String {
498        (**self).stub_literal()
499    }
500}
501
502impl StubLiteral for String {
503    fn stub_literal(&self) -> String {
504        self.as_str().stub_literal()
505    }
506}
507
508impl StubLiteral for std::borrow::Cow<'_, str> {
509    fn stub_literal(&self) -> String {
510        self.as_ref().stub_literal()
511    }
512}
513
514impl<T: StubLiteral> StubLiteral for Option<T> {
515    fn stub_literal(&self) -> String {
516        self.as_ref()
517            .map_or_else(|| "null".to_string(), StubLiteral::stub_literal)
518    }
519}
520
521impl<T: StubLiteral> StubLiteral for [T] {
522    fn stub_literal(&self) -> String {
523        let items: Vec<String> = self.iter().map(StubLiteral::stub_literal).collect();
524        format!("[{}]", items.join(", "))
525    }
526}
527
528impl<T: StubLiteral> StubLiteral for Vec<T> {
529    fn stub_literal(&self) -> String {
530        self.as_slice().stub_literal()
531    }
532}
533
534impl<T: StubLiteral, const N: usize> StubLiteral for [T; N] {
535    fn stub_literal(&self) -> String {
536        self.as_slice().stub_literal()
537    }
538}
539
540impl<K: StubLiteral, V: StubLiteral, S> StubLiteral for std::collections::HashMap<K, V, S> {
541    fn stub_literal(&self) -> String {
542        let mut entries: Vec<String> = self
543            .iter()
544            .map(|(key, value)| format!("{} => {}", key.stub_literal(), value.stub_literal()))
545            .collect();
546        entries.sort();
547        format!("[{}]", entries.join(", "))
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    use crate::types::{PhpRef, Separated, ZendHashTable, ZendObject};
555
556    const _: () = {
557        assert!(<Option<i64> as FromZval>::NULLABLE);
558        assert!(<Option<i64> as FromZvalMut>::NULLABLE);
559        assert!(!<i64 as FromZval>::NULLABLE);
560        assert!(!<i64 as FromZvalMut>::NULLABLE);
561        assert!(!<i64 as FromZvalMut>::BY_REF);
562        assert!(!<&Zval as FromZvalMut>::BY_REF);
563        assert!(<&mut Zval as FromZvalMut>::BY_REF);
564        assert!(<PhpRef<'_> as FromZvalMut>::BY_REF);
565        assert!(!<Separated<'_> as FromZvalMut>::BY_REF);
566        assert!(<&mut ZendHashTable as FromZvalMut>::BY_REF);
567        assert!(!<&mut ZendObject as FromZvalMut>::BY_REF);
568    };
569
570    #[test]
571    fn stub_literals_are_php_source() {
572        assert_eq!(42_i32.stub_literal(), "42");
573        assert_eq!(true.stub_literal(), "true");
574        assert_eq!(1.5_f64.stub_literal(), "1.5");
575        assert_eq!(3.0_f64.stub_literal(), "3.0");
576        assert_eq!(f64::INFINITY.stub_literal(), "INF");
577        assert_eq!("it's".stub_literal(), "'it\\'s'");
578        assert_eq!(String::from("a\\b").stub_literal(), "'a\\\\b'");
579        assert_eq!(None::<i64>.stub_literal(), "null");
580        assert_eq!(Some("x".to_string()).stub_literal(), "'x'");
581        assert_eq!(Vec::<i64>::new().stub_literal(), "[]");
582        assert_eq!(vec![1, 2].stub_literal(), "[1, 2]");
583        let map: std::collections::HashMap<&str, i64> = [("b", 2), ("a", 1)].into();
584        assert_eq!(map.stub_literal(), "['a' => 1, 'b' => 2]");
585    }
586
587    #[test]
588    fn omitted_argument_is_none_for_option_and_missing_otherwise() {
589        assert_eq!(<Option<i64> as FromZvalMut>::from_missing(), Some(None));
590        assert_eq!(
591            <Option<Option<i64>> as FromZvalMut>::from_missing(),
592            Some(None)
593        );
594        assert_eq!(<i64 as FromZvalMut>::from_missing(), None);
595    }
596}