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