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}