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