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}