• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

extphprs / ext-php-rs / 21292814460

23 Jan 2026 04:12PM UTC coverage: 35.004% (-1.0%) from 36.028%
21292814460

Pull #636

github

web-flow
Merge 92748b471 into 579986816
Pull Request #636: feat(object): Lazy ghost and Lazy Proxy

0 of 150 new or added lines in 5 files covered. (0.0%)

48 existing lines in 2 files now uncovered.

1823 of 5208 relevant lines covered (35.0%)

14.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

8.57
/src/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

49
use std::{convert::TryInto, fmt::Debug, os::raw::c_char, ptr};
50

51
use 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)]
68
use crate::ffi::{zend_lazy_object_init, zend_lazy_object_mark_as_initialized};
69

70
#[cfg(all(feature = "closure", php84))]
71
use 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.
85
pub type ZendObject = zend_object;
86

87
impl 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 {
6✔
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.")
18✔
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> {
1✔
173
        unsafe {
174
            self.handlers()?
2✔
175
                .get_class_name
176
                .and_then(|f| f(self).as_ref())
3✔
177
                .ok_or(Error::InvalidScope)
2✔
178
                .and_then(TryInto::try_into)
1✔
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 {
4✔
191
        self.get_class_entry().instance_of(ce)
12✔
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 {
4✔
210
        self.instance_of(ce::traversable())
12✔
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]
NEW
429
    pub fn is_lazy(&self) -> bool {
×
430
        // Check extra_flags directly - safe for all objects
NEW
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]
NEW
442
    pub fn is_lazy_proxy(&self) -> bool {
×
443
        // Check extra_flags directly - safe for all objects
NEW
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]
NEW
455
    pub fn is_lazy_ghost(&self) -> bool {
×
456
        // A lazy ghost has IS_OBJ_LAZY_UNINITIALIZED set but NOT IS_OBJ_LAZY_PROXY
NEW
457
        (self.extra_flags & Self::IS_OBJ_LAZY_UNINITIALIZED) != 0
×
NEW
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]
NEW
468
    pub fn is_lazy_initialized(&self) -> bool {
×
NEW
469
        if !self.is_lazy() {
×
NEW
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
NEW
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]
NEW
488
    pub fn lazy_init(&mut self) -> Option<&mut Self> {
×
NEW
489
        if !self.is_lazy() {
×
NEW
490
            return None;
×
491
        }
NEW
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]
NEW
505
    pub fn mark_lazy_initialized(&mut self) -> Option<&mut Self> {
×
NEW
506
        if !self.is_lazy() {
×
NEW
507
            return None;
×
508
        }
NEW
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]
NEW
519
    pub fn lazy_get_instance(&mut self) -> Option<&mut Self> {
×
NEW
520
        if !self.is_lazy_proxy() || !self.is_lazy_initialized() {
×
NEW
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.
NEW
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)]
NEW
575
    pub fn make_lazy_ghost<F>(&mut self, initializer: F) -> Result<()>
×
576
    where
577
        F: Fn() + 'static,
578
    {
NEW
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)]
NEW
628
    pub fn make_lazy_proxy<F>(&mut self, initializer: F) -> Result<()>
×
629
    where
630
        F: Fn() -> Option<ZBox<ZendObject>> + 'static,
631
    {
NEW
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))]
NEW
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
NEW
643
        let ce = unsafe { self.ce.as_ref() }.ok_or(Error::InvalidPointer)?;
×
NEW
644
        if !ce.can_be_lazy() {
×
NEW
645
            return Err(Error::LazyObjectFailed);
×
646
        }
647

648
        // Wrap the Rust closure in a PHP-callable Closure
NEW
649
        let closure = Closure::wrap(Box::new(initializer) as Box<dyn Fn() -> R>);
×
650

651
        // Convert the closure to a zval
NEW
652
        let mut initializer_zv = Zval::new();
×
NEW
653
        closure.set_zval(&mut initializer_zv, false)?;
×
654

655
        // Initialize the fcc structure
NEW
656
        let mut fcc: _zend_fcall_info_cache = unsafe { std::mem::zeroed() };
×
657

658
        // Populate the fcc using zend_is_callable_ex
659
        let is_callable = unsafe {
660
            zend_is_callable_ex(
NEW
661
                &raw mut initializer_zv,
×
NEW
662
                ptr::null_mut(),
×
663
                0,
NEW
664
                ptr::null_mut(),
×
NEW
665
                &raw mut fcc,
×
NEW
666
                ptr::null_mut(),
×
667
            )
668
        };
669

NEW
670
        if !is_callable {
×
NEW
671
            return Err(Error::Callable);
×
672
        }
673

674
        // Get the class entry
NEW
675
        let ce = self.ce;
×
676

677
        // Make the object lazy
678
        let result = unsafe {
NEW
679
            zend_object_make_lazy(self, ce, &raw mut initializer_zv, &raw mut fcc, strategy)
×
680
        };
681

NEW
682
        if result.is_null() {
×
NEW
683
            Err(Error::LazyObjectFailed)
×
684
        } else {
NEW
685
            Ok(())
×
686
        }
687
    }
688

689
    /// Attempts to retrieve a reference to the object handlers.
690
    #[inline]
691
    unsafe fn handlers(&self) -> Result<&ZendObjectHandlers> {
1✔
692
        unsafe { self.handlers.as_ref() }.ok_or(Error::InvalidScope)
4✔
693
    }
694

695
    /// Returns a mutable pointer to `self`, regardless of the type of
696
    /// reference. Only to be used in situations where a C function requires
697
    /// a mutable pointer but does not modify the underlying data.
698
    #[inline]
UNCOV
699
    fn mut_ptr(&self) -> *mut Self {
×
UNCOV
700
        ptr::from_ref(self).cast_mut()
×
701
    }
702
}
703

704
unsafe impl ZBoxable for ZendObject {
705
    fn free(&mut self) {
1✔
706
        unsafe { ext_php_rs_zend_object_release(self) }
2✔
707
    }
708
}
709

710
impl Debug for ZendObject {
UNCOV
711
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
UNCOV
712
        let mut dbg = f.debug_struct(
×
UNCOV
713
            self.get_class_name()
×
UNCOV
714
                .unwrap_or_else(|_| "ZendObject".to_string())
×
UNCOV
715
                .as_str(),
×
716
        );
717

718
        if let Ok(props) = self.get_properties() {
×
719
            for (key, val) in props {
×
720
                dbg.field(key.to_string().as_str(), val);
×
721
            }
722
        }
723

724
        dbg.finish()
×
725
    }
726
}
727

728
impl<'a> FromZval<'a> for &'a ZendObject {
729
    const TYPE: DataType = DataType::Object(None);
730

UNCOV
731
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
732
        zval.object()
×
733
    }
734
}
735

736
impl<'a> FromZvalMut<'a> for &'a mut ZendObject {
737
    const TYPE: DataType = DataType::Object(None);
738

UNCOV
739
    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
×
UNCOV
740
        zval.object_mut()
×
741
    }
742
}
743

744
impl IntoZval for ZBox<ZendObject> {
745
    const TYPE: DataType = DataType::Object(None);
746
    const NULLABLE: bool = false;
747

748
    #[inline]
UNCOV
749
    fn set_zval(mut self, zv: &mut Zval, _: bool) -> Result<()> {
×
750
        // We must decrement the refcounter on the object before inserting into the
751
        // zval, as the reference counter will be incremented on add.
752
        // NOTE(david): again is this needed, we increment in `set_object`.
UNCOV
753
        self.dec_count();
×
754
        zv.set_object(self.into_raw());
×
UNCOV
755
        Ok(())
×
756
    }
757
}
758

759
impl IntoZval for &mut ZendObject {
760
    const TYPE: DataType = DataType::Object(None);
761
    const NULLABLE: bool = false;
762

763
    #[inline]
UNCOV
764
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
UNCOV
765
        zv.set_object(self);
×
UNCOV
766
        Ok(())
×
767
    }
768
}
769

770
impl FromZendObject<'_> for String {
771
    fn from_zend_object(obj: &ZendObject) -> Result<Self> {
×
UNCOV
772
        let mut ret = Zval::new();
×
773
        unsafe {
774
            zend_call_known_function(
UNCOV
775
                (*obj.ce).__tostring,
×
776
                ptr::from_ref(obj).cast_mut(),
×
777
                obj.ce,
×
UNCOV
778
                &raw mut ret,
×
779
                0,
780
                ptr::null_mut(),
×
781
                ptr::null_mut(),
×
782
            );
783
        }
784

785
        if let Some(err) = ExecutorGlobals::take_exception() {
×
786
            // TODO: become an error
UNCOV
787
            let class_name = obj.get_class_name();
×
UNCOV
788
            panic!(
×
789
                "Uncaught exception during call to {}::__toString(): {:?}",
790
                class_name.expect("unable to determine class name"),
×
791
                err
792
            );
793
        } else if let Some(output) = ret.extract() {
×
UNCOV
794
            Ok(output)
×
795
        } else {
796
            // TODO: become an error
UNCOV
797
            let class_name = obj.get_class_name();
×
798
            panic!(
×
799
                "{}::__toString() must return a string",
UNCOV
800
                class_name.expect("unable to determine class name"),
×
801
            );
802
        }
803
    }
804
}
805

806
impl<T: RegisteredClass> From<ZBox<ZendClassObject<T>>> for ZBox<ZendObject> {
807
    #[inline]
UNCOV
808
    fn from(obj: ZBox<ZendClassObject<T>>) -> Self {
×
UNCOV
809
        ZendObject::from_class_object(obj)
×
810
    }
811
}
812

813
/// Different ways to query if a property exists.
814
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
815
#[repr(u32)]
816
pub enum PropertyQuery {
817
    /// Property exists and is not NULL.
818
    Isset = ZEND_PROPERTY_ISSET,
819
    /// Property is not empty.
820
    NotEmpty = ZEND_ISEMPTY,
821
    /// Property exists.
822
    Exists = ZEND_PROPERTY_EXISTS,
823
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc