ext_php_rs/types/
object.rs

1//! Represents an object in PHP. Allows for overriding the internal object used
2//! by classes, allowing users to store Rust data inside a PHP object.
3
4use std::{convert::TryInto, fmt::Debug, os::raw::c_char, ptr};
5
6use crate::{
7    boxed::{ZBox, ZBoxable},
8    class::RegisteredClass,
9    convert::{FromZendObject, FromZval, FromZvalMut, IntoZval, IntoZvalDyn},
10    error::{Error, Result},
11    ffi::{
12        ext_php_rs_zend_object_release, object_properties_init, zend_call_known_function,
13        zend_function, zend_hash_str_find_ptr_lc, zend_object, zend_objects_new, HashTable,
14        ZEND_ISEMPTY, ZEND_PROPERTY_EXISTS, ZEND_PROPERTY_ISSET,
15    },
16    flags::DataType,
17    rc::PhpRc,
18    types::{ZendClassObject, ZendStr, Zval},
19    zend::{ce, ClassEntry, ExecutorGlobals, ZendObjectHandlers},
20};
21
22/// A PHP object.
23///
24/// This type does not maintain any information about its type, for example,
25/// classes with have associated Rust structs cannot be accessed through this
26/// type. [`ZendClassObject`] is used for this purpose, and you can convert
27/// between the two.
28pub type ZendObject = zend_object;
29
30impl ZendObject {
31    /// Creates a new [`ZendObject`], returned inside an [`ZBox<ZendObject>`]
32    /// wrapper.
33    ///
34    /// # Parameters
35    ///
36    /// * `ce` - The type of class the new object should be an instance of.
37    ///
38    /// # Panics
39    ///
40    /// Panics when allocating memory for the new object fails.
41    #[must_use]
42    pub fn new(ce: &ClassEntry) -> ZBox<Self> {
43        // SAFETY: Using emalloc to allocate memory inside Zend arena. Casting `ce` to
44        // `*mut` is valid as the function will not mutate `ce`.
45        unsafe {
46            let ptr = match ce.__bindgen_anon_2.create_object {
47                None => {
48                    let ptr = zend_objects_new(ptr::from_ref(ce).cast_mut());
49                    assert!(!ptr.is_null(), "Failed to allocate memory for Zend object");
50
51                    object_properties_init(ptr, ptr::from_ref(ce).cast_mut());
52                    ptr
53                }
54                Some(v) => v(ptr::from_ref(ce).cast_mut()),
55            };
56
57            ZBox::from_raw(
58                ptr.as_mut()
59                    .expect("Failed to allocate memory for Zend object"),
60            )
61        }
62    }
63
64    /// Creates a new `stdClass` instance, returned inside an
65    /// [`ZBox<ZendObject>`] wrapper.
66    ///
67    /// # Panics
68    ///
69    /// Panics if allocating memory for the object fails, or if the `stdClass`
70    /// class entry has not been registered with PHP yet.
71    ///
72    /// # Example
73    ///
74    /// ```no_run
75    /// use ext_php_rs::types::ZendObject;
76    ///
77    /// let mut obj = ZendObject::new_stdclass();
78    ///
79    /// obj.set_property("hello", "world");
80    /// ```
81    #[must_use]
82    pub fn new_stdclass() -> ZBox<Self> {
83        // SAFETY: This will be `NULL` until it is initialized. `as_ref()` checks for
84        // null, so we can panic if it's null.
85        Self::new(ce::stdclass())
86    }
87
88    /// Converts a class object into an owned [`ZendObject`]. This removes any
89    /// possibility of accessing the underlying attached Rust struct.
90    #[must_use]
91    pub fn from_class_object<T: RegisteredClass>(obj: ZBox<ZendClassObject<T>>) -> ZBox<Self> {
92        let this = obj.into_raw();
93        // SAFETY: Consumed box must produce a well-aligned non-null pointer.
94        unsafe { ZBox::from_raw(this.get_mut_zend_obj()) }
95    }
96
97    /// Returns the [`ClassEntry`] associated with this object.
98    ///
99    /// # Panics
100    ///
101    /// Panics if the class entry is invalid.
102    #[must_use]
103    pub fn get_class_entry(&self) -> &'static ClassEntry {
104        // SAFETY: it is OK to panic here since PHP would segfault anyway
105        // when encountering an object with no class entry.
106        unsafe { self.ce.as_ref() }.expect("Could not retrieve class entry.")
107    }
108
109    /// Attempts to retrieve the class name of the object.
110    ///
111    /// # Errors
112    ///
113    /// * `Error::InvalidScope` - If the object handlers or the class name
114    ///   cannot be retrieved.
115    pub fn get_class_name(&self) -> Result<String> {
116        unsafe {
117            self.handlers()?
118                .get_class_name
119                .and_then(|f| f(self).as_ref())
120                .ok_or(Error::InvalidScope)
121                .and_then(TryInto::try_into)
122        }
123    }
124
125    /// Returns whether this object is an instance of the given [`ClassEntry`].
126    ///
127    /// This method checks the class and interface inheritance chain.
128    ///
129    /// # Panics
130    ///
131    /// Panics if the class entry is invalid.
132    #[must_use]
133    pub fn instance_of(&self, ce: &ClassEntry) -> bool {
134        self.get_class_entry().instance_of(ce)
135    }
136
137    /// Checks if the given object is an instance of a registered class with
138    /// Rust type `T`.
139    ///
140    /// This method doesn't check the class and interface inheritance chain.
141    #[must_use]
142    pub fn is_instance<T: RegisteredClass>(&self) -> bool {
143        (self.ce.cast_const()).eq(&ptr::from_ref(T::get_metadata().ce()))
144    }
145
146    /// Returns whether this object is an instance of \Traversable
147    ///
148    /// # Panics
149    ///
150    /// Panics if the class entry is invalid.
151    #[must_use]
152    pub fn is_traversable(&self) -> bool {
153        self.instance_of(ce::traversable())
154    }
155
156    /// Tries to call a method on the object.
157    ///
158    /// # Returns
159    ///
160    /// Returns the return value of the method, or an error if the method
161    /// could not be found or called.
162    ///
163    /// # Errors
164    ///
165    /// * `Error::Callable` - If the method could not be found.
166    /// * If a parameter could not be converted to a zval.
167    /// * If the parameter count is bigger than `u32::MAX`.
168    // TODO: Measure this
169    #[allow(clippy::inline_always)]
170    #[inline(always)]
171    pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
172        let mut retval = Zval::new();
173        let len = params.len();
174        let params = params
175            .into_iter()
176            .map(|val| val.as_zval(false))
177            .collect::<Result<Vec<_>>>()?;
178        let packed = params.into_boxed_slice();
179
180        unsafe {
181            let res = zend_hash_str_find_ptr_lc(
182                &raw const (*self.ce).function_table,
183                name.as_ptr().cast::<c_char>(),
184                name.len(),
185            )
186            .cast::<zend_function>();
187
188            if res.is_null() {
189                return Err(Error::Callable);
190            }
191
192            zend_call_known_function(
193                res,
194                ptr::from_ref(self).cast_mut(),
195                self.ce,
196                &raw mut retval,
197                len.try_into()?,
198                packed.as_ptr().cast_mut(),
199                std::ptr::null_mut(),
200            );
201        };
202
203        Ok(retval)
204    }
205
206    /// Attempts to read a property from the Object. Returns a result containing
207    /// the value of the property if it exists and can be read, and an
208    /// [`Error`] otherwise.
209    ///
210    /// # Parameters
211    ///
212    /// * `name` - The name of the property.
213    /// * `query` - The type of query to use when attempting to get a property.
214    ///
215    /// # Errors
216    ///
217    /// * `Error::InvalidScope` - If the object handlers or the properties
218    ///   cannot be retrieved.
219    pub fn get_property<'a, T>(&'a self, name: &str) -> Result<T>
220    where
221        T: FromZval<'a>,
222    {
223        if !self.has_property(name, PropertyQuery::Exists)? {
224            return Err(Error::InvalidProperty);
225        }
226
227        let mut name = ZendStr::new(name, false);
228        let mut rv = Zval::new();
229
230        let zv = unsafe {
231            self.handlers()?.read_property.ok_or(Error::InvalidScope)?(
232                self.mut_ptr(),
233                &raw mut *name,
234                1,
235                ptr::null_mut(),
236                &raw mut rv,
237            )
238            .as_ref()
239        }
240        .ok_or(Error::InvalidScope)?;
241
242        T::from_zval(zv).ok_or_else(|| Error::ZvalConversion(zv.get_type()))
243    }
244
245    /// Attempts to set a property on the object.
246    ///
247    /// # Parameters
248    ///
249    /// * `name` - The name of the property.
250    /// * `value` - The value to set the property to.
251    ///
252    /// # Errors
253    ///
254    /// * `Error::InvalidScope` - If the object handlers or the properties
255    ///   cannot be retrieved.
256    pub fn set_property(&mut self, name: &str, value: impl IntoZval) -> Result<()> {
257        let mut name = ZendStr::new(name, false);
258        let mut value = value.into_zval(false)?;
259
260        unsafe {
261            self.handlers()?.write_property.ok_or(Error::InvalidScope)?(
262                self,
263                &raw mut *name,
264                &raw mut value,
265                ptr::null_mut(),
266            )
267            .as_ref()
268        }
269        .ok_or(Error::InvalidScope)?;
270        Ok(())
271    }
272
273    /// Checks if a property exists on an object. Takes a property name and
274    /// query parameter, which defines what classifies if a property exists
275    /// or not. See [`PropertyQuery`] for more information.
276    ///
277    /// # Parameters
278    ///
279    /// * `name` - The name of the property.
280    /// * `query` - The 'query' to classify if a property exists.
281    ///
282    /// # Errors
283    ///
284    /// * `Error::InvalidScope` - If the object handlers or the properties
285    ///   cannot be retrieved.
286    pub fn has_property(&self, name: &str, query: PropertyQuery) -> Result<bool> {
287        let mut name = ZendStr::new(name, false);
288
289        Ok(unsafe {
290            self.handlers()?.has_property.ok_or(Error::InvalidScope)?(
291                self.mut_ptr(),
292                &raw mut *name,
293                query as _,
294                std::ptr::null_mut(),
295            )
296        } > 0)
297    }
298
299    /// Attempts to retrieve the properties of the object. Returned inside a
300    /// Zend Hashtable.
301    ///
302    /// # Errors
303    ///
304    /// * `Error::InvalidScope` - If the object handlers or the properties
305    ///   cannot be retrieved.
306    pub fn get_properties(&self) -> Result<&HashTable> {
307        unsafe {
308            self.handlers()?
309                .get_properties
310                .and_then(|props| props(self.mut_ptr()).as_ref())
311                .ok_or(Error::InvalidScope)
312        }
313    }
314
315    /// Extracts some type from a Zend object.
316    ///
317    /// This is a wrapper function around `FromZendObject::extract()`.
318    ///
319    /// # Errors
320    ///
321    /// Returns an error if the conversion fails.
322    pub fn extract<'a, T>(&'a self) -> Result<T>
323    where
324        T: FromZendObject<'a>,
325    {
326        T::from_zend_object(self)
327    }
328
329    /// Returns an unique identifier for the object.
330    ///
331    /// The id is guaranteed to be unique for the lifetime of the object.
332    /// Once the object is destroyed, it may be reused for other objects.
333    /// This is equivalent to calling the [`spl_object_id`] PHP function.
334    ///
335    /// [`spl_object_id`]: https://www.php.net/manual/function.spl-object-id
336    #[inline]
337    #[must_use]
338    pub fn get_id(&self) -> u32 {
339        self.handle
340    }
341
342    /// Computes an unique hash for the object.
343    ///
344    /// The hash is guaranteed to be unique for the lifetime of the object.
345    /// Once the object is destroyed, it may be reused for other objects.
346    /// This is equivalent to calling the [`spl_object_hash`] PHP function.
347    ///
348    /// [`spl_object_hash`]: https://www.php.net/manual/function.spl-object-hash.php
349    #[must_use]
350    pub fn hash(&self) -> String {
351        format!("{:016x}0000000000000000", self.handle)
352    }
353
354    /// Attempts to retrieve a reference to the object handlers.
355    #[inline]
356    unsafe fn handlers(&self) -> Result<&ZendObjectHandlers> {
357        self.handlers.as_ref().ok_or(Error::InvalidScope)
358    }
359
360    /// Returns a mutable pointer to `self`, regardless of the type of
361    /// reference. Only to be used in situations where a C function requires
362    /// a mutable pointer but does not modify the underlying data.
363    #[inline]
364    fn mut_ptr(&self) -> *mut Self {
365        ptr::from_ref(self).cast_mut()
366    }
367}
368
369unsafe impl ZBoxable for ZendObject {
370    fn free(&mut self) {
371        unsafe { ext_php_rs_zend_object_release(self) }
372    }
373}
374
375impl Debug for ZendObject {
376    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377        let mut dbg = f.debug_struct(
378            self.get_class_name()
379                .unwrap_or_else(|_| "ZendObject".to_string())
380                .as_str(),
381        );
382
383        if let Ok(props) = self.get_properties() {
384            for (key, val) in props {
385                dbg.field(key.to_string().as_str(), val);
386            }
387        }
388
389        dbg.finish()
390    }
391}
392
393impl<'a> FromZval<'a> for &'a ZendObject {
394    const TYPE: DataType = DataType::Object(None);
395
396    fn from_zval(zval: &'a Zval) -> Option<Self> {
397        zval.object()
398    }
399}
400
401impl<'a> FromZvalMut<'a> for &'a mut ZendObject {
402    const TYPE: DataType = DataType::Object(None);
403
404    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
405        zval.object_mut()
406    }
407}
408
409impl IntoZval for ZBox<ZendObject> {
410    const TYPE: DataType = DataType::Object(None);
411    const NULLABLE: bool = false;
412
413    #[inline]
414    fn set_zval(mut self, zv: &mut Zval, _: bool) -> Result<()> {
415        // We must decrement the refcounter on the object before inserting into the
416        // zval, as the reference counter will be incremented on add.
417        // NOTE(david): again is this needed, we increment in `set_object`.
418        self.dec_count();
419        zv.set_object(self.into_raw());
420        Ok(())
421    }
422}
423
424impl IntoZval for &mut ZendObject {
425    const TYPE: DataType = DataType::Object(None);
426    const NULLABLE: bool = false;
427
428    #[inline]
429    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
430        zv.set_object(self);
431        Ok(())
432    }
433}
434
435impl FromZendObject<'_> for String {
436    fn from_zend_object(obj: &ZendObject) -> Result<Self> {
437        let mut ret = Zval::new();
438        unsafe {
439            zend_call_known_function(
440                (*obj.ce).__tostring,
441                ptr::from_ref(obj).cast_mut(),
442                obj.ce,
443                &raw mut ret,
444                0,
445                ptr::null_mut(),
446                ptr::null_mut(),
447            );
448        }
449
450        if let Some(err) = ExecutorGlobals::take_exception() {
451            // TODO: become an error
452            let class_name = obj.get_class_name();
453            panic!(
454                "Uncaught exception during call to {}::__toString(): {:?}",
455                class_name.expect("unable to determine class name"),
456                err
457            );
458        } else if let Some(output) = ret.extract() {
459            Ok(output)
460        } else {
461            // TODO: become an error
462            let class_name = obj.get_class_name();
463            panic!(
464                "{}::__toString() must return a string",
465                class_name.expect("unable to determine class name"),
466            );
467        }
468    }
469}
470
471impl<T: RegisteredClass> From<ZBox<ZendClassObject<T>>> for ZBox<ZendObject> {
472    #[inline]
473    fn from(obj: ZBox<ZendClassObject<T>>) -> Self {
474        ZendObject::from_class_object(obj)
475    }
476}
477
478/// Different ways to query if a property exists.
479#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
480#[repr(u32)]
481pub enum PropertyQuery {
482    /// Property exists and is not NULL.
483    Isset = ZEND_PROPERTY_ISSET,
484    /// Property is not empty.
485    NotEmpty = ZEND_ISEMPTY,
486    /// Property exists.
487    Exists = ZEND_PROPERTY_EXISTS,
488}