Skip to main content

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//!
4//! # Lazy Objects (PHP 8.4+)
5//!
6//! PHP 8.4 introduced lazy objects, which defer their initialization until
7//! their properties are first accessed. This module provides introspection APIs
8//! for lazy objects:
9//!
10//! - [`ZendObject::is_lazy()`] - Check if an object is lazy (ghost or proxy)
11//! - [`ZendObject::is_lazy_ghost()`] - Check if an object is a lazy ghost
12//! - [`ZendObject::is_lazy_proxy()`] - Check if an object is a lazy proxy
13//! - [`ZendObject::is_lazy_initialized()`] - Check if a lazy object has been initialized
14//! - [`ZendObject::lazy_init()`] - Trigger initialization of a lazy object
15//!
16//! ## Lazy Ghosts vs Lazy Proxies
17//!
18//! - **Lazy Ghosts**: The ghost object itself becomes the real instance when
19//!   initialized. After initialization, the ghost is indistinguishable from a
20//!   regular object (the `is_lazy()` flag is cleared).
21//!
22//! - **Lazy Proxies**: A proxy wraps a real instance that is created when first
23//!   accessed. The proxy and real instance have different identities. After
24//!   initialization, the proxy still reports as lazy (`is_lazy()` returns true).
25//!
26//! ## Creating Lazy Objects
27//!
28//! Lazy objects should be created using PHP's `ReflectionClass` API:
29//!
30//! ```php
31//! <?php
32//! // Create a lazy ghost
33//! $reflector = new ReflectionClass(MyClass::class);
34//! $ghost = $reflector->newLazyGhost(function ($obj) {
35//!     $obj->__construct('initialized');
36//! });
37//!
38//! // Create a lazy proxy
39//! $proxy = $reflector->newLazyProxy(function ($obj) {
40//!     return new MyClass('initialized');
41//! });
42//! ```
43//!
44//! **Note**: PHP 8.4 lazy objects only work with user-defined PHP classes, not
45//! internal classes. Since Rust-defined classes (using `#[php_class]`) are
46//! registered as internal classes, they cannot be made lazy using PHP's
47//! Reflection API.
48
49use std::{convert::TryInto, fmt::Debug, os::raw::c_char, ptr};
50
51use crate::{
52    boxed::{ZBox, ZBoxable},
53    class::RegisteredClass,
54    convert::{FromZendObject, FromZval, FromZvalMut, IntoZval, IntoZvalDyn},
55    error::{Error, Result},
56    ffi::{
57        HashTable, ZEND_ISEMPTY, ZEND_PROPERTY_EXISTS, ZEND_PROPERTY_ISSET,
58        ext_php_rs_zend_object_release, object_properties_init, zend_call_known_function,
59        zend_function, zend_hash_str_find_ptr_lc, zend_object, zend_objects_new,
60    },
61    flags::DataType,
62    rc::PhpRc,
63    types::{ZendClassObject, ZendStr, Zval},
64    zend::{ClassEntry, ExecutorGlobals, ZendObjectHandlers, ce},
65};
66
67#[cfg(php84)]
68use crate::ffi::{zend_lazy_object_init, zend_lazy_object_mark_as_initialized};
69
70#[cfg(all(feature = "closure", php84))]
71use crate::{
72    closure::Closure,
73    ffi::{
74        _zend_fcall_info_cache, ZEND_LAZY_OBJECT_STRATEGY_GHOST, ZEND_LAZY_OBJECT_STRATEGY_PROXY,
75        zend_is_callable_ex, zend_object_make_lazy,
76    },
77};
78
79/// A PHP object.
80///
81/// This type does not maintain any information about its type, for example,
82/// classes with have associated Rust structs cannot be accessed through this
83/// type. [`ZendClassObject`] is used for this purpose, and you can convert
84/// between the two.
85pub type ZendObject = zend_object;
86
87impl ZendObject {
88    /// Creates a new [`ZendObject`], returned inside an [`ZBox<ZendObject>`]
89    /// wrapper.
90    ///
91    /// # Parameters
92    ///
93    /// * `ce` - The type of class the new object should be an instance of.
94    ///
95    /// # Panics
96    ///
97    /// Panics when allocating memory for the new object fails.
98    #[must_use]
99    pub fn new(ce: &ClassEntry) -> ZBox<Self> {
100        // SAFETY: Using emalloc to allocate memory inside Zend arena. Casting `ce` to
101        // `*mut` is valid as the function will not mutate `ce`.
102        unsafe {
103            let ptr = match ce.__bindgen_anon_2.create_object {
104                None => {
105                    let ptr = zend_objects_new(ptr::from_ref(ce).cast_mut());
106                    assert!(!ptr.is_null(), "Failed to allocate memory for Zend object");
107
108                    object_properties_init(ptr, ptr::from_ref(ce).cast_mut());
109                    ptr
110                }
111                Some(v) => v(ptr::from_ref(ce).cast_mut()),
112            };
113
114            ZBox::from_raw(
115                ptr.as_mut()
116                    .expect("Failed to allocate memory for Zend object"),
117            )
118        }
119    }
120
121    /// Creates a new `stdClass` instance, returned inside an
122    /// [`ZBox<ZendObject>`] wrapper.
123    ///
124    /// # Panics
125    ///
126    /// Panics if allocating memory for the object fails, or if the `stdClass`
127    /// class entry has not been registered with PHP yet.
128    ///
129    /// # Example
130    ///
131    /// ```no_run
132    /// use ext_php_rs::types::ZendObject;
133    ///
134    /// let mut obj = ZendObject::new_stdclass();
135    ///
136    /// obj.set_property("hello", "world");
137    /// ```
138    #[must_use]
139    pub fn new_stdclass() -> ZBox<Self> {
140        // SAFETY: This will be `NULL` until it is initialized. `as_ref()` checks for
141        // null, so we can panic if it's null.
142        Self::new(ce::stdclass())
143    }
144
145    /// Converts a class object into an owned [`ZendObject`]. This removes any
146    /// possibility of accessing the underlying attached Rust struct.
147    #[must_use]
148    pub fn from_class_object<T: RegisteredClass>(obj: ZBox<ZendClassObject<T>>) -> ZBox<Self> {
149        let this = obj.into_raw();
150        // SAFETY: Consumed box must produce a well-aligned non-null pointer.
151        unsafe { ZBox::from_raw(this.get_mut_zend_obj()) }
152    }
153
154    /// Returns the [`ClassEntry`] associated with this object.
155    ///
156    /// # Panics
157    ///
158    /// Panics if the class entry is invalid.
159    #[must_use]
160    pub fn get_class_entry(&self) -> &'static ClassEntry {
161        // SAFETY: it is OK to panic here since PHP would segfault anyway
162        // when encountering an object with no class entry.
163        unsafe { self.ce.as_ref() }.expect("Could not retrieve class entry.")
164    }
165
166    /// Attempts to retrieve the class name of the object.
167    ///
168    /// # Errors
169    ///
170    /// * `Error::InvalidScope` - If the object handlers or the class name
171    ///   cannot be retrieved.
172    pub fn get_class_name(&self) -> Result<String> {
173        unsafe {
174            self.handlers()?
175                .get_class_name
176                .and_then(|f| f(self).as_ref())
177                .ok_or(Error::InvalidScope)
178                .and_then(TryInto::try_into)
179        }
180    }
181
182    /// Returns whether this object is an instance of the given [`ClassEntry`].
183    ///
184    /// This method checks the class and interface inheritance chain.
185    ///
186    /// # Panics
187    ///
188    /// Panics if the class entry is invalid.
189    #[must_use]
190    pub fn instance_of(&self, ce: &ClassEntry) -> bool {
191        self.get_class_entry().instance_of(ce)
192    }
193
194    /// Checks if the given object is an instance of a registered class with
195    /// Rust type `T`.
196    ///
197    /// This method doesn't check the class and interface inheritance chain.
198    #[must_use]
199    pub fn is_instance<T: RegisteredClass>(&self) -> bool {
200        (self.ce.cast_const()).eq(&ptr::from_ref(T::get_metadata().ce()))
201    }
202
203    /// Returns whether this object is an instance of \Traversable
204    ///
205    /// # Panics
206    ///
207    /// Panics if the class entry is invalid.
208    #[must_use]
209    pub fn is_traversable(&self) -> bool {
210        self.instance_of(ce::traversable())
211    }
212
213    /// Tries to call a method on the object.
214    ///
215    /// # Returns
216    ///
217    /// Returns the return value of the method, or an error if the method
218    /// could not be found or called.
219    ///
220    /// # Errors
221    ///
222    /// * `Error::Callable` - If the method could not be found.
223    /// * If a parameter could not be converted to a zval.
224    /// * If the parameter count is bigger than `u32::MAX`.
225    // TODO: Measure this
226    #[allow(clippy::inline_always)]
227    #[inline(always)]
228    pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
229        let mut retval = Zval::new();
230        let len = params.len();
231        let params = params
232            .into_iter()
233            .map(|val| val.as_zval(false))
234            .collect::<Result<Vec<_>>>()?;
235        let packed = params.into_boxed_slice();
236
237        unsafe {
238            let res = zend_hash_str_find_ptr_lc(
239                &raw const (*self.ce).function_table,
240                name.as_ptr().cast::<c_char>(),
241                name.len(),
242            )
243            .cast::<zend_function>();
244
245            if res.is_null() {
246                return Err(Error::Callable);
247            }
248
249            zend_call_known_function(
250                res,
251                ptr::from_ref(self).cast_mut(),
252                self.ce,
253                &raw mut retval,
254                len.try_into()?,
255                packed.as_ptr().cast_mut(),
256                std::ptr::null_mut(),
257            );
258        };
259
260        Ok(retval)
261    }
262
263    /// Attempts to read a property from the Object. Returns a result containing
264    /// the value of the property if it exists and can be read, and an
265    /// [`Error`] otherwise.
266    ///
267    /// # Parameters
268    ///
269    /// * `name` - The name of the property.
270    /// * `query` - The type of query to use when attempting to get a property.
271    ///
272    /// # Errors
273    ///
274    /// * `Error::InvalidScope` - If the object handlers or the properties
275    ///   cannot be retrieved.
276    pub fn get_property<'a, T>(&'a self, name: &str) -> Result<T>
277    where
278        T: FromZval<'a>,
279    {
280        if !self.has_property(name, PropertyQuery::Exists)? {
281            return Err(Error::InvalidProperty);
282        }
283
284        let mut name = ZendStr::new(name, false);
285        let mut rv = Zval::new();
286
287        let zv = unsafe {
288            self.handlers()?.read_property.ok_or(Error::InvalidScope)?(
289                self.mut_ptr(),
290                &raw mut *name,
291                1,
292                ptr::null_mut(),
293                &raw mut rv,
294            )
295            .as_ref()
296        }
297        .ok_or(Error::InvalidScope)?;
298
299        T::from_zval(zv).ok_or_else(|| Error::ZvalConversion(zv.get_type()))
300    }
301
302    /// Attempts to set a property on the object.
303    ///
304    /// # Parameters
305    ///
306    /// * `name` - The name of the property.
307    /// * `value` - The value to set the property to.
308    ///
309    /// # Errors
310    ///
311    /// * `Error::InvalidScope` - If the object handlers or the properties
312    ///   cannot be retrieved.
313    pub fn set_property(&mut self, name: &str, value: impl IntoZval) -> Result<()> {
314        let mut name = ZendStr::new(name, false);
315        let mut value = value.into_zval(false)?;
316
317        unsafe {
318            self.handlers()?.write_property.ok_or(Error::InvalidScope)?(
319                self,
320                &raw mut *name,
321                &raw mut value,
322                ptr::null_mut(),
323            )
324            .as_ref()
325        }
326        .ok_or(Error::InvalidScope)?;
327        Ok(())
328    }
329
330    /// Checks if a property exists on an object. Takes a property name and
331    /// query parameter, which defines what classifies if a property exists
332    /// or not. See [`PropertyQuery`] for more information.
333    ///
334    /// # Parameters
335    ///
336    /// * `name` - The name of the property.
337    /// * `query` - The 'query' to classify if a property exists.
338    ///
339    /// # Errors
340    ///
341    /// * `Error::InvalidScope` - If the object handlers or the properties
342    ///   cannot be retrieved.
343    pub fn has_property(&self, name: &str, query: PropertyQuery) -> Result<bool> {
344        let mut name = ZendStr::new(name, false);
345
346        Ok(unsafe {
347            self.handlers()?.has_property.ok_or(Error::InvalidScope)?(
348                self.mut_ptr(),
349                &raw mut *name,
350                query as _,
351                std::ptr::null_mut(),
352            )
353        } > 0)
354    }
355
356    /// Attempts to retrieve the properties of the object. Returned inside a
357    /// Zend Hashtable.
358    ///
359    /// # Errors
360    ///
361    /// * `Error::InvalidScope` - If the object handlers or the properties
362    ///   cannot be retrieved.
363    pub fn get_properties(&self) -> Result<&HashTable> {
364        unsafe {
365            self.handlers()?
366                .get_properties
367                .and_then(|props| props(self.mut_ptr()).as_ref())
368                .ok_or(Error::InvalidScope)
369        }
370    }
371
372    /// Extracts some type from a Zend object.
373    ///
374    /// This is a wrapper function around `FromZendObject::extract()`.
375    ///
376    /// # Errors
377    ///
378    /// Returns an error if the conversion fails.
379    pub fn extract<'a, T>(&'a self) -> Result<T>
380    where
381        T: FromZendObject<'a>,
382    {
383        T::from_zend_object(self)
384    }
385
386    /// Returns an unique identifier for the object.
387    ///
388    /// The id is guaranteed to be unique for the lifetime of the object.
389    /// Once the object is destroyed, it may be reused for other objects.
390    /// This is equivalent to calling the [`spl_object_id`] PHP function.
391    ///
392    /// [`spl_object_id`]: https://www.php.net/manual/function.spl-object-id
393    #[inline]
394    #[must_use]
395    pub fn get_id(&self) -> u32 {
396        self.handle
397    }
398
399    /// Computes an unique hash for the object.
400    ///
401    /// The hash is guaranteed to be unique for the lifetime of the object.
402    /// Once the object is destroyed, it may be reused for other objects.
403    /// This is equivalent to calling the [`spl_object_hash`] PHP function.
404    ///
405    /// [`spl_object_hash`]: https://www.php.net/manual/function.spl-object-hash.php
406    #[must_use]
407    pub fn hash(&self) -> String {
408        format!("{:016x}0000000000000000", self.handle)
409    }
410
411    // Object extra_flags constants for lazy object detection.
412    // PHP 8.4+ lazy object constants
413    // These are checked on zend_object.extra_flags before calling zend_lazy_object_get_flags.
414    // IS_OBJ_LAZY_UNINITIALIZED = (1U<<31) - Virtual proxy or uninitialized Ghost
415    #[cfg(php84)]
416    const IS_OBJ_LAZY_UNINITIALIZED: u32 = 1 << 31;
417    // IS_OBJ_LAZY_PROXY = (1U<<30) - Virtual proxy (may be initialized)
418    #[cfg(php84)]
419    const IS_OBJ_LAZY_PROXY: u32 = 1 << 30;
420
421    /// Returns whether this object is a lazy object (ghost or proxy).
422    ///
423    /// Lazy objects are objects whose initialization is deferred until
424    /// one of their properties is accessed.
425    ///
426    /// This is a PHP 8.4+ feature.
427    #[cfg(php84)]
428    #[must_use]
429    pub fn is_lazy(&self) -> bool {
430        // Check extra_flags directly - safe for all objects
431        (self.extra_flags & (Self::IS_OBJ_LAZY_UNINITIALIZED | Self::IS_OBJ_LAZY_PROXY)) != 0
432    }
433
434    /// Returns whether this object is a lazy proxy.
435    ///
436    /// Lazy proxies wrap a real instance that is created when the proxy
437    /// is first accessed. The proxy and real instance have different identities.
438    ///
439    /// This is a PHP 8.4+ feature.
440    #[cfg(php84)]
441    #[must_use]
442    pub fn is_lazy_proxy(&self) -> bool {
443        // Check extra_flags directly - safe for all objects
444        (self.extra_flags & Self::IS_OBJ_LAZY_PROXY) != 0
445    }
446
447    /// Returns whether this object is a lazy ghost.
448    ///
449    /// Lazy ghosts are indistinguishable from non-lazy objects once initialized.
450    /// The ghost object itself becomes the real instance.
451    ///
452    /// This is a PHP 8.4+ feature.
453    #[cfg(php84)]
454    #[must_use]
455    pub fn is_lazy_ghost(&self) -> bool {
456        // A lazy ghost has IS_OBJ_LAZY_UNINITIALIZED set but NOT IS_OBJ_LAZY_PROXY
457        (self.extra_flags & Self::IS_OBJ_LAZY_UNINITIALIZED) != 0
458            && (self.extra_flags & Self::IS_OBJ_LAZY_PROXY) == 0
459    }
460
461    /// Returns whether this lazy object has been initialized.
462    ///
463    /// Returns `false` for non-lazy objects.
464    ///
465    /// This is a PHP 8.4+ feature.
466    #[cfg(php84)]
467    #[must_use]
468    pub fn is_lazy_initialized(&self) -> bool {
469        if !self.is_lazy() {
470            return false;
471        }
472        // A lazy object is initialized when IS_OBJ_LAZY_UNINITIALIZED is NOT set.
473        // For ghosts: both flags clear when initialized
474        // For proxies: IS_OBJ_LAZY_PROXY stays but IS_OBJ_LAZY_UNINITIALIZED clears
475        (self.extra_flags & Self::IS_OBJ_LAZY_UNINITIALIZED) == 0
476    }
477
478    /// Triggers initialization of a lazy object.
479    ///
480    /// If the object is a lazy ghost, this populates the object in place.
481    /// If the object is a lazy proxy, this creates the real instance.
482    ///
483    /// Returns `None` if the object is not lazy or initialization fails.
484    ///
485    /// This is a PHP 8.4+ feature.
486    #[cfg(php84)]
487    #[must_use]
488    pub fn lazy_init(&mut self) -> Option<&mut Self> {
489        if !self.is_lazy() {
490            return None;
491        }
492        unsafe { zend_lazy_object_init(self).as_mut() }
493    }
494
495    /// Marks a lazy object as initialized without calling the initializer.
496    ///
497    /// This can be used to manually initialize a lazy object's properties
498    /// and then mark it as initialized.
499    ///
500    /// Returns `None` if the object is not lazy.
501    ///
502    /// This is a PHP 8.4+ feature.
503    #[cfg(php84)]
504    #[must_use]
505    pub fn mark_lazy_initialized(&mut self) -> Option<&mut Self> {
506        if !self.is_lazy() {
507            return None;
508        }
509        unsafe { zend_lazy_object_mark_as_initialized(self).as_mut() }
510    }
511
512    /// For lazy proxies, returns the real instance after initialization.
513    ///
514    /// Returns `None` if this is not a lazy proxy or if not initialized.
515    ///
516    /// This is a PHP 8.4+ feature.
517    #[cfg(php84)]
518    #[must_use]
519    pub fn lazy_get_instance(&mut self) -> Option<&mut Self> {
520        if !self.is_lazy_proxy() || !self.is_lazy_initialized() {
521            return None;
522        }
523        // Note: We use zend_lazy_object_init here because zend_lazy_object_get_instance
524        // is not exported (no ZEND_API) in PHP and cannot be linked on Windows.
525        // zend_lazy_object_init returns the real instance for already-initialized proxies.
526        unsafe { zend_lazy_object_init(self).as_mut() }
527    }
528
529    /// Converts this object into a lazy ghost with the given initializer.
530    ///
531    /// The initializer closure will be called when the object's properties are
532    /// first accessed. The closure should perform initialization logic.
533    ///
534    /// # Parameters
535    ///
536    /// * `initializer` - A closure that performs initialization. The closure
537    ///   returns `()`. Any state needed for initialization should be captured
538    ///   in the closure.
539    ///
540    /// # Returns
541    ///
542    /// Returns `Ok(())` if the object was successfully made lazy, or an error
543    /// if the operation failed.
544    ///
545    /// # Example
546    ///
547    /// ```rust,no_run
548    /// use ext_php_rs::types::ZendObject;
549    ///
550    /// fn make_lazy_example(obj: &mut ZendObject) -> ext_php_rs::error::Result<()> {
551    ///     let init_value = "initialized".to_string();
552    ///     obj.make_lazy_ghost(Box::new(move || {
553    ///         // Use captured state for initialization
554    ///         println!("Initializing with: {}", init_value);
555    ///     }) as Box<dyn Fn()>)?;
556    ///     Ok(())
557    /// }
558    /// ```
559    ///
560    /// # Errors
561    ///
562    /// Returns an error if the initializer closure cannot be converted to a
563    /// PHP callable or if the object cannot be made lazy.
564    ///
565    /// # Safety
566    ///
567    /// This is a PHP 8.4+ feature. The closure must be `'static` as it may be
568    /// called at any time during the object's lifetime.
569    ///
570    /// **Note**: PHP 8.4 lazy objects only work with user-defined PHP classes,
571    /// not internal classes. Rust-defined classes cannot be made lazy.
572    #[cfg(all(feature = "closure", php84))]
573    #[cfg_attr(docs, doc(cfg(all(feature = "closure", php84))))]
574    #[allow(clippy::cast_possible_truncation)]
575    pub fn make_lazy_ghost<F>(&mut self, initializer: F) -> Result<()>
576    where
577        F: Fn() + 'static,
578    {
579        self.make_lazy_internal(initializer, ZEND_LAZY_OBJECT_STRATEGY_GHOST as u8)
580    }
581
582    /// Converts this object into a lazy proxy with the given initializer.
583    ///
584    /// The initializer closure will be called when the object's properties are
585    /// first accessed. The closure should return the real instance that the
586    /// proxy will forward to.
587    ///
588    /// # Parameters
589    ///
590    /// * `initializer` - A closure that returns `Option<ZBox<ZendObject>>`,
591    ///   the real instance. Any state needed should be captured in the closure.
592    ///
593    /// # Returns
594    ///
595    /// Returns `Ok(())` if the object was successfully made lazy, or an error
596    /// if the operation failed.
597    ///
598    /// # Example
599    ///
600    /// ```rust,no_run
601    /// use ext_php_rs::types::ZendObject;
602    /// use ext_php_rs::boxed::ZBox;
603    ///
604    /// fn make_proxy_example(obj: &mut ZendObject) -> ext_php_rs::error::Result<()> {
605    ///     obj.make_lazy_proxy(Box::new(|| {
606    ///         // Create and return the real instance
607    ///         Some(ZendObject::new_stdclass())
608    ///     }) as Box<dyn Fn() -> Option<ZBox<ZendObject>>>)?;
609    ///     Ok(())
610    /// }
611    /// ```
612    ///
613    /// # Errors
614    ///
615    /// Returns an error if the initializer closure cannot be converted to a
616    /// PHP callable or if the object cannot be made lazy.
617    ///
618    /// # Safety
619    ///
620    /// This is a PHP 8.4+ feature. The closure must be `'static` as it may be
621    /// called at any time during the object's lifetime.
622    ///
623    /// **Note**: PHP 8.4 lazy objects only work with user-defined PHP classes,
624    /// not internal classes. Rust-defined classes cannot be made lazy.
625    #[cfg(all(feature = "closure", php84))]
626    #[cfg_attr(docs, doc(cfg(all(feature = "closure", php84))))]
627    #[allow(clippy::cast_possible_truncation)]
628    pub fn make_lazy_proxy<F>(&mut self, initializer: F) -> Result<()>
629    where
630        F: Fn() -> Option<ZBox<ZendObject>> + 'static,
631    {
632        self.make_lazy_internal(initializer, ZEND_LAZY_OBJECT_STRATEGY_PROXY as u8)
633    }
634
635    /// Internal implementation for making an object lazy.
636    #[cfg(all(feature = "closure", php84))]
637    fn make_lazy_internal<F, R>(&mut self, initializer: F, strategy: u8) -> Result<()>
638    where
639        F: Fn() -> R + 'static,
640        R: IntoZval + 'static,
641    {
642        // Check if the class can be made lazy
643        let ce = unsafe { self.ce.as_ref() }.ok_or(Error::InvalidPointer)?;
644        if !ce.can_be_lazy() {
645            return Err(Error::LazyObjectFailed);
646        }
647
648        // Cannot make an already-lazy uninitialized object lazy again
649        if self.is_lazy() && !self.is_lazy_initialized() {
650            return Err(Error::LazyObjectFailed);
651        }
652
653        // Wrap the Rust closure in a PHP-callable Closure
654        let closure = Closure::wrap(Box::new(initializer) as Box<dyn Fn() -> R>);
655
656        // Convert the closure to a zval
657        let mut initializer_zv = Zval::new();
658        closure.set_zval(&mut initializer_zv, false)?;
659
660        // Initialize the fcc structure
661        let mut fcc: _zend_fcall_info_cache = unsafe { std::mem::zeroed() };
662
663        // Populate the fcc using zend_is_callable_ex
664        let is_callable = unsafe {
665            zend_is_callable_ex(
666                &raw mut initializer_zv,
667                ptr::null_mut(),
668                0,
669                ptr::null_mut(),
670                &raw mut fcc,
671                ptr::null_mut(),
672            )
673        };
674
675        if !is_callable {
676            return Err(Error::Callable);
677        }
678
679        // Get the class entry
680        let ce = self.ce;
681
682        // Make the object lazy
683        let result = unsafe {
684            zend_object_make_lazy(self, ce, &raw mut initializer_zv, &raw mut fcc, strategy)
685        };
686
687        if result.is_null() {
688            Err(Error::LazyObjectFailed)
689        } else {
690            Ok(())
691        }
692    }
693
694    /// Attempts to retrieve a reference to the object handlers.
695    #[inline]
696    unsafe fn handlers(&self) -> Result<&ZendObjectHandlers> {
697        unsafe { self.handlers.as_ref() }.ok_or(Error::InvalidScope)
698    }
699
700    /// Returns a mutable pointer to `self`, regardless of the type of
701    /// reference. Only to be used in situations where a C function requires
702    /// a mutable pointer but does not modify the underlying data.
703    #[inline]
704    fn mut_ptr(&self) -> *mut Self {
705        ptr::from_ref(self).cast_mut()
706    }
707}
708
709unsafe impl ZBoxable for ZendObject {
710    fn free(&mut self) {
711        unsafe { ext_php_rs_zend_object_release(self) }
712    }
713}
714
715impl Debug for ZendObject {
716    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
717        let mut dbg = f.debug_struct(
718            self.get_class_name()
719                .unwrap_or_else(|_| "ZendObject".to_string())
720                .as_str(),
721        );
722
723        if let Ok(props) = self.get_properties() {
724            for (key, val) in props {
725                dbg.field(key.to_string().as_str(), val);
726            }
727        }
728
729        dbg.finish()
730    }
731}
732
733impl<'a> FromZval<'a> for &'a ZendObject {
734    const TYPE: DataType = DataType::Object(None);
735
736    fn from_zval(zval: &'a Zval) -> Option<Self> {
737        zval.object()
738    }
739}
740
741impl<'a> FromZvalMut<'a> for &'a mut ZendObject {
742    const TYPE: DataType = DataType::Object(None);
743
744    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
745        zval.object_mut()
746    }
747}
748
749impl IntoZval for ZBox<ZendObject> {
750    const TYPE: DataType = DataType::Object(None);
751    const NULLABLE: bool = false;
752
753    #[inline]
754    fn set_zval(mut self, zv: &mut Zval, _: bool) -> Result<()> {
755        // We must decrement the refcounter on the object before inserting into the
756        // zval, as the reference counter will be incremented on add.
757        // NOTE(david): again is this needed, we increment in `set_object`.
758        self.dec_count();
759        zv.set_object(self.into_raw());
760        Ok(())
761    }
762}
763
764impl IntoZval for &mut ZendObject {
765    const TYPE: DataType = DataType::Object(None);
766    const NULLABLE: bool = false;
767
768    #[inline]
769    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
770        zv.set_object(self);
771        Ok(())
772    }
773}
774
775impl FromZendObject<'_> for String {
776    fn from_zend_object(obj: &ZendObject) -> Result<Self> {
777        let mut ret = Zval::new();
778        unsafe {
779            zend_call_known_function(
780                (*obj.ce).__tostring,
781                ptr::from_ref(obj).cast_mut(),
782                obj.ce,
783                &raw mut ret,
784                0,
785                ptr::null_mut(),
786                ptr::null_mut(),
787            );
788        }
789
790        if let Some(err) = ExecutorGlobals::take_exception() {
791            // TODO: become an error
792            let class_name = obj.get_class_name();
793            panic!(
794                "Uncaught exception during call to {}::__toString(): {:?}",
795                class_name.expect("unable to determine class name"),
796                err
797            );
798        } else if let Some(output) = ret.extract() {
799            Ok(output)
800        } else {
801            // TODO: become an error
802            let class_name = obj.get_class_name();
803            panic!(
804                "{}::__toString() must return a string",
805                class_name.expect("unable to determine class name"),
806            );
807        }
808    }
809}
810
811impl<T: RegisteredClass> From<ZBox<ZendClassObject<T>>> for ZBox<ZendObject> {
812    #[inline]
813    fn from(obj: ZBox<ZendClassObject<T>>) -> Self {
814        ZendObject::from_class_object(obj)
815    }
816}
817
818/// Different ways to query if a property exists.
819#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
820#[repr(u32)]
821pub enum PropertyQuery {
822    /// Property exists and is not NULL.
823    Isset = ZEND_PROPERTY_ISSET,
824    /// Property is not empty.
825    NotEmpty = ZEND_ISEMPTY,
826    /// Property exists.
827    Exists = ZEND_PROPERTY_EXISTS,
828}