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