diff --git a/allowed_bindings.rs b/allowed_bindings.rs index b1596ad11..d06913945 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -106,6 +106,9 @@ bind! { zend_hash_str_update, zend_internal_arg_info, zend_is_callable, + zend_is_callable_ex, + zend_fcall_info_cache, + _zend_fcall_info_cache, zend_is_identical, zend_is_iterable, zend_known_strings, @@ -261,6 +264,18 @@ bind! { zend_std_get_properties, zend_std_has_property, zend_objects_new, + zend_object_make_lazy, + zend_lazy_object_init, + zend_lazy_object_mark_as_initialized, + // Note: zend_lazy_object_get_instance and zend_lazy_object_get_flags are not + // exported (no ZEND_API) in PHP, so they cannot be used on Windows. + // Use zend_lazy_object_init instead which returns the instance for proxies. + zend_class_can_be_lazy, + zend_lazy_object_flags_t, + ZEND_LAZY_OBJECT_STRATEGY_GHOST, + ZEND_LAZY_OBJECT_STRATEGY_PROXY, + ZEND_LAZY_OBJECT_INITIALIZED, + ZEND_LAZY_OBJECT_SKIP_INITIALIZATION_ON_SERIALIZE, zend_standard_class_def, zend_class_serialize_deny, zend_class_unserialize_deny, diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 853d6664b..60773f9a1 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -972,7 +972,8 @@ fn expr_to_php_stub(expr: &Expr) -> String { } } -/// Returns true if the given type is nullable in PHP (i.e., it's an `Option`). +/// Returns true if the given type is nullable in PHP (i.e., it's an +/// `Option`). /// /// Note: Having a default value does NOT make a type nullable. A parameter with /// a default value is optional (can be omitted), but passing `null` explicitly diff --git a/docsrs_bindings.rs b/docsrs_bindings.rs index 7c096da27..ad3c65521 100644 --- a/docsrs_bindings.rs +++ b/docsrs_bindings.rs @@ -189,6 +189,10 @@ pub const E_STRICT: u32 = 2048; pub const E_RECOVERABLE_ERROR: u32 = 4096; pub const E_DEPRECATED: u32 = 8192; pub const E_USER_DEPRECATED: u32 = 16384; +pub const ZEND_LAZY_OBJECT_STRATEGY_PROXY: u32 = 1; +pub const ZEND_LAZY_OBJECT_STRATEGY_GHOST: u32 = 2; +pub const ZEND_LAZY_OBJECT_INITIALIZED: u32 = 4; +pub const ZEND_LAZY_OBJECT_SKIP_INITIALIZATION_ON_SERIALIZE: u32 = 8; pub const ZEND_PROPERTY_ISSET: u32 = 0; pub const ZEND_PROPERTY_EXISTS: u32 = 2; pub const ZEND_ACC_PUBLIC: u32 = 1; @@ -1211,6 +1215,7 @@ pub type zend_error_handling_t = ::std::os::raw::c_uint; pub const zend_property_hook_kind_ZEND_PROPERTY_HOOK_GET: zend_property_hook_kind = 0; pub const zend_property_hook_kind_ZEND_PROPERTY_HOOK_SET: zend_property_hook_kind = 1; pub type zend_property_hook_kind = ::std::os::raw::c_uint; +pub type zend_lazy_object_flags_t = u8; #[repr(C)] pub struct _zend_lazy_objects_store { pub infos: HashTable, @@ -1218,6 +1223,24 @@ pub struct _zend_lazy_objects_store { pub type zend_lazy_objects_store = _zend_lazy_objects_store; pub type zend_property_info = _zend_property_info; pub type zend_fcall_info_cache = _zend_fcall_info_cache; +unsafe extern "C" { + pub fn zend_class_can_be_lazy(ce: *const zend_class_entry) -> bool; +} +unsafe extern "C" { + pub fn zend_object_make_lazy( + obj: *mut zend_object, + class_type: *mut zend_class_entry, + initializer_zv: *mut zval, + initializer_fcc: *mut zend_fcall_info_cache, + flags: zend_lazy_object_flags_t, + ) -> *mut zend_object; +} +unsafe extern "C" { + pub fn zend_lazy_object_init(obj: *mut zend_object) -> *mut zend_object; +} +unsafe extern "C" { + pub fn zend_lazy_object_mark_as_initialized(obj: *mut zend_object) -> *mut zend_object; +} pub type zend_object_read_property_t = ::std::option::Option< unsafe extern "C" fn( object: *mut zend_object, @@ -2084,6 +2107,16 @@ unsafe extern "C" { orig_class_entry: *mut zend_class_entry, ) -> *mut zend_class_entry; } +unsafe extern "C" { + pub fn zend_is_callable_ex( + callable: *mut zval, + object: *mut zend_object, + check_flags: u32, + callable_name: *mut *mut zend_string, + fcc: *mut zend_fcall_info_cache, + error: *mut *mut ::std::os::raw::c_char, + ) -> bool; +} unsafe extern "C" { pub fn zend_is_callable( callable: *mut zval, diff --git a/guide/src/types/object.md b/guide/src/types/object.md index 479a55e72..c6f114167 100644 --- a/guide/src/types/object.md +++ b/guide/src/types/object.md @@ -78,4 +78,136 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { # fn main() {} ``` +## Lazy Objects (PHP 8.4+) + +PHP 8.4 introduced lazy objects, which defer their initialization until their +properties are first accessed. ext-php-rs provides APIs to introspect and create +lazy objects from Rust. + +### Lazy Object Types + +There are two types of lazy objects: + +- **Lazy Ghosts**: The ghost object itself becomes the real instance when + initialized. After initialization, the ghost is indistinguishable from a + regular object. + +- **Lazy Proxies**: A proxy wraps a real instance that is created when first + accessed. The proxy and real instance have different identities. After + initialization, the proxy still reports as lazy. + +### Introspection APIs + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::{prelude::*, types::ZendObject}; + +#[php_function] +pub fn check_lazy(obj: &ZendObject) -> String { + if obj.is_lazy() { + if obj.is_lazy_ghost() { + if obj.is_lazy_initialized() { + "Initialized lazy ghost".into() + } else { + "Uninitialized lazy ghost".into() + } + } else if obj.is_lazy_proxy() { + if obj.is_lazy_initialized() { + "Initialized lazy proxy".into() + } else { + "Uninitialized lazy proxy".into() + } + } else { + "Unknown lazy type".into() + } + } else { + "Not a lazy object".into() + } +} +# fn main() {} +``` + +Available introspection methods: + +| Method | Description | +|--------|-------------| +| `is_lazy()` | Returns `true` if the object is lazy (ghost or proxy) | +| `is_lazy_ghost()` | Returns `true` if the object is a lazy ghost | +| `is_lazy_proxy()` | Returns `true` if the object is a lazy proxy | +| `is_lazy_initialized()` | Returns `true` if the lazy object has been initialized | +| `lazy_init()` | Triggers initialization of a lazy object | +| `lazy_get_instance()` | For proxies, returns the real instance after initialization | + +### Creating Lazy Objects from Rust + +You can create lazy objects from Rust using `make_lazy_ghost()` and +`make_lazy_proxy()`. These methods require the `closure` feature: + +```toml +[dependencies] +ext-php-rs = { version = "0.15", features = ["closure"] } +``` + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::{prelude::*, types::ZendObject, boxed::ZBox}; + +#[php_function] +pub fn create_lazy_ghost(obj: &mut ZendObject) -> PhpResult<()> { + let init_value = "initialized".to_string(); + obj.make_lazy_ghost(Box::new(move || { + // Initialization logic - use captured state + println!("Initializing with: {}", init_value); + }) as Box)?; + Ok(()) +} + +#[php_function] +pub fn create_lazy_proxy(obj: &mut ZendObject) -> PhpResult<()> { + obj.make_lazy_proxy(Box::new(|| { + // Return the real instance + Some(ZendObject::new_stdclass()) + }) as Box Option>>)?; + Ok(()) +} +# fn main() {} +``` + +### Creating Lazy Objects from PHP + +For full control over lazy object creation, use PHP's Reflection API: + +```php +newLazyGhost(function ($obj) { + $obj->__construct('initialized'); +}); + +// Create a lazy proxy +$proxy = $reflector->newLazyProxy(function ($obj) { + return new MyClass('initialized'); +}); +``` + +### Limitations + +- **PHP 8.4+ only**: Lazy objects are a PHP 8.4 feature and not available in + earlier versions. + +- **`closure` feature required**: The `make_lazy_ghost()` and `make_lazy_proxy()` + methods require the `closure` feature to be enabled. + +- **User-defined classes only**: PHP lazy objects only work with user-defined + PHP classes, not internal classes. Since Rust-defined classes (using + `#[php_class]`) are registered as internal classes, they cannot be made lazy. + +- **Closure parameter access**: Due to Rust trait system limitations, the + `make_lazy_ghost()` and `make_lazy_proxy()` closures don't receive the object + being initialized as a parameter. Capture any needed initialization state in + the closure itself. + [class object]: ./class_object.md diff --git a/src/types/object.rs b/src/types/object.rs index d305dfb34..749e94050 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -1,5 +1,50 @@ //! Represents an object in PHP. Allows for overriding the internal object used //! by classes, allowing users to store Rust data inside a PHP object. +//! +//! # Lazy Objects (PHP 8.4+) +//! +//! PHP 8.4 introduced lazy objects, which defer their initialization until +//! their properties are first accessed. This module provides introspection APIs +//! for lazy objects: +//! +//! - [`ZendObject::is_lazy()`] - Check if an object is lazy (ghost or proxy) +//! - [`ZendObject::is_lazy_ghost()`] - Check if an object is a lazy ghost +//! - [`ZendObject::is_lazy_proxy()`] - Check if an object is a lazy proxy +//! - [`ZendObject::is_lazy_initialized()`] - Check if a lazy object has been initialized +//! - [`ZendObject::lazy_init()`] - Trigger initialization of a lazy object +//! +//! ## Lazy Ghosts vs Lazy Proxies +//! +//! - **Lazy Ghosts**: The ghost object itself becomes the real instance when +//! initialized. After initialization, the ghost is indistinguishable from a +//! regular object (the `is_lazy()` flag is cleared). +//! +//! - **Lazy Proxies**: A proxy wraps a real instance that is created when first +//! accessed. The proxy and real instance have different identities. After +//! initialization, the proxy still reports as lazy (`is_lazy()` returns true). +//! +//! ## Creating Lazy Objects +//! +//! Lazy objects should be created using PHP's `ReflectionClass` API: +//! +//! ```php +//! newLazyGhost(function ($obj) { +//! $obj->__construct('initialized'); +//! }); +//! +//! // Create a lazy proxy +//! $proxy = $reflector->newLazyProxy(function ($obj) { +//! return new MyClass('initialized'); +//! }); +//! ``` +//! +//! **Note**: PHP 8.4 lazy objects only work with user-defined PHP classes, not +//! internal classes. Since Rust-defined classes (using `#[php_class]`) are +//! registered as internal classes, they cannot be made lazy using PHP's +//! Reflection API. use std::{convert::TryInto, fmt::Debug, os::raw::c_char, ptr}; @@ -19,6 +64,18 @@ use crate::{ zend::{ClassEntry, ExecutorGlobals, ZendObjectHandlers, ce}, }; +#[cfg(php84)] +use crate::ffi::{zend_lazy_object_init, zend_lazy_object_mark_as_initialized}; + +#[cfg(all(feature = "closure", php84))] +use crate::{ + closure::Closure, + ffi::{ + _zend_fcall_info_cache, ZEND_LAZY_OBJECT_STRATEGY_GHOST, ZEND_LAZY_OBJECT_STRATEGY_PROXY, + zend_is_callable_ex, zend_object_make_lazy, + }, +}; + /// A PHP object. /// /// This type does not maintain any information about its type, for example, @@ -351,6 +408,278 @@ impl ZendObject { format!("{:016x}0000000000000000", self.handle) } + // Object extra_flags constants for lazy object detection. + // PHP 8.4+ lazy object constants + // These are checked on zend_object.extra_flags before calling zend_lazy_object_get_flags. + // IS_OBJ_LAZY_UNINITIALIZED = (1U<<31) - Virtual proxy or uninitialized Ghost + #[cfg(php84)] + const IS_OBJ_LAZY_UNINITIALIZED: u32 = 1 << 31; + // IS_OBJ_LAZY_PROXY = (1U<<30) - Virtual proxy (may be initialized) + #[cfg(php84)] + const IS_OBJ_LAZY_PROXY: u32 = 1 << 30; + + /// Returns whether this object is a lazy object (ghost or proxy). + /// + /// Lazy objects are objects whose initialization is deferred until + /// one of their properties is accessed. + /// + /// This is a PHP 8.4+ feature. + #[cfg(php84)] + #[must_use] + pub fn is_lazy(&self) -> bool { + // Check extra_flags directly - safe for all objects + (self.extra_flags & (Self::IS_OBJ_LAZY_UNINITIALIZED | Self::IS_OBJ_LAZY_PROXY)) != 0 + } + + /// Returns whether this object is a lazy proxy. + /// + /// Lazy proxies wrap a real instance that is created when the proxy + /// is first accessed. The proxy and real instance have different identities. + /// + /// This is a PHP 8.4+ feature. + #[cfg(php84)] + #[must_use] + pub fn is_lazy_proxy(&self) -> bool { + // Check extra_flags directly - safe for all objects + (self.extra_flags & Self::IS_OBJ_LAZY_PROXY) != 0 + } + + /// Returns whether this object is a lazy ghost. + /// + /// Lazy ghosts are indistinguishable from non-lazy objects once initialized. + /// The ghost object itself becomes the real instance. + /// + /// This is a PHP 8.4+ feature. + #[cfg(php84)] + #[must_use] + pub fn is_lazy_ghost(&self) -> bool { + // A lazy ghost has IS_OBJ_LAZY_UNINITIALIZED set but NOT IS_OBJ_LAZY_PROXY + (self.extra_flags & Self::IS_OBJ_LAZY_UNINITIALIZED) != 0 + && (self.extra_flags & Self::IS_OBJ_LAZY_PROXY) == 0 + } + + /// Returns whether this lazy object has been initialized. + /// + /// Returns `false` for non-lazy objects. + /// + /// This is a PHP 8.4+ feature. + #[cfg(php84)] + #[must_use] + pub fn is_lazy_initialized(&self) -> bool { + if !self.is_lazy() { + return false; + } + // A lazy object is initialized when IS_OBJ_LAZY_UNINITIALIZED is NOT set. + // For ghosts: both flags clear when initialized + // For proxies: IS_OBJ_LAZY_PROXY stays but IS_OBJ_LAZY_UNINITIALIZED clears + (self.extra_flags & Self::IS_OBJ_LAZY_UNINITIALIZED) == 0 + } + + /// Triggers initialization of a lazy object. + /// + /// If the object is a lazy ghost, this populates the object in place. + /// If the object is a lazy proxy, this creates the real instance. + /// + /// Returns `None` if the object is not lazy or initialization fails. + /// + /// This is a PHP 8.4+ feature. + #[cfg(php84)] + #[must_use] + pub fn lazy_init(&mut self) -> Option<&mut Self> { + if !self.is_lazy() { + return None; + } + unsafe { zend_lazy_object_init(self).as_mut() } + } + + /// Marks a lazy object as initialized without calling the initializer. + /// + /// This can be used to manually initialize a lazy object's properties + /// and then mark it as initialized. + /// + /// Returns `None` if the object is not lazy. + /// + /// This is a PHP 8.4+ feature. + #[cfg(php84)] + #[must_use] + pub fn mark_lazy_initialized(&mut self) -> Option<&mut Self> { + if !self.is_lazy() { + return None; + } + unsafe { zend_lazy_object_mark_as_initialized(self).as_mut() } + } + + /// For lazy proxies, returns the real instance after initialization. + /// + /// Returns `None` if this is not a lazy proxy or if not initialized. + /// + /// This is a PHP 8.4+ feature. + #[cfg(php84)] + #[must_use] + pub fn lazy_get_instance(&mut self) -> Option<&mut Self> { + if !self.is_lazy_proxy() || !self.is_lazy_initialized() { + return None; + } + // Note: We use zend_lazy_object_init here because zend_lazy_object_get_instance + // is not exported (no ZEND_API) in PHP and cannot be linked on Windows. + // zend_lazy_object_init returns the real instance for already-initialized proxies. + unsafe { zend_lazy_object_init(self).as_mut() } + } + + /// Converts this object into a lazy ghost with the given initializer. + /// + /// The initializer closure will be called when the object's properties are + /// first accessed. The closure should perform initialization logic. + /// + /// # Parameters + /// + /// * `initializer` - A closure that performs initialization. The closure + /// returns `()`. Any state needed for initialization should be captured + /// in the closure. + /// + /// # Returns + /// + /// Returns `Ok(())` if the object was successfully made lazy, or an error + /// if the operation failed. + /// + /// # Example + /// + /// ```rust,no_run + /// use ext_php_rs::types::ZendObject; + /// + /// fn make_lazy_example(obj: &mut ZendObject) -> ext_php_rs::error::Result<()> { + /// let init_value = "initialized".to_string(); + /// obj.make_lazy_ghost(Box::new(move || { + /// // Use captured state for initialization + /// println!("Initializing with: {}", init_value); + /// }) as Box)?; + /// Ok(()) + /// } + /// ``` + /// + /// # Errors + /// + /// Returns an error if the initializer closure cannot be converted to a + /// PHP callable or if the object cannot be made lazy. + /// + /// # Safety + /// + /// This is a PHP 8.4+ feature. The closure must be `'static` as it may be + /// called at any time during the object's lifetime. + /// + /// **Note**: PHP 8.4 lazy objects only work with user-defined PHP classes, + /// not internal classes. Rust-defined classes cannot be made lazy. + #[cfg(all(feature = "closure", php84))] + #[cfg_attr(docs, doc(cfg(all(feature = "closure", php84))))] + #[allow(clippy::cast_possible_truncation)] + pub fn make_lazy_ghost(&mut self, initializer: F) -> Result<()> + where + F: Fn() + 'static, + { + self.make_lazy_internal(initializer, ZEND_LAZY_OBJECT_STRATEGY_GHOST as u8) + } + + /// Converts this object into a lazy proxy with the given initializer. + /// + /// The initializer closure will be called when the object's properties are + /// first accessed. The closure should return the real instance that the + /// proxy will forward to. + /// + /// # Parameters + /// + /// * `initializer` - A closure that returns `Option>`, + /// the real instance. Any state needed should be captured in the closure. + /// + /// # Returns + /// + /// Returns `Ok(())` if the object was successfully made lazy, or an error + /// if the operation failed. + /// + /// # Example + /// + /// ```rust,no_run + /// use ext_php_rs::types::ZendObject; + /// use ext_php_rs::boxed::ZBox; + /// + /// fn make_proxy_example(obj: &mut ZendObject) -> ext_php_rs::error::Result<()> { + /// obj.make_lazy_proxy(Box::new(|| { + /// // Create and return the real instance + /// Some(ZendObject::new_stdclass()) + /// }) as Box Option>>)?; + /// Ok(()) + /// } + /// ``` + /// + /// # Errors + /// + /// Returns an error if the initializer closure cannot be converted to a + /// PHP callable or if the object cannot be made lazy. + /// + /// # Safety + /// + /// This is a PHP 8.4+ feature. The closure must be `'static` as it may be + /// called at any time during the object's lifetime. + /// + /// **Note**: PHP 8.4 lazy objects only work with user-defined PHP classes, + /// not internal classes. Rust-defined classes cannot be made lazy. + #[cfg(all(feature = "closure", php84))] + #[cfg_attr(docs, doc(cfg(all(feature = "closure", php84))))] + #[allow(clippy::cast_possible_truncation)] + pub fn make_lazy_proxy(&mut self, initializer: F) -> Result<()> + where + F: Fn() -> Option> + 'static, + { + self.make_lazy_internal(initializer, ZEND_LAZY_OBJECT_STRATEGY_PROXY as u8) + } + + /// Internal implementation for making an object lazy. + #[cfg(all(feature = "closure", php84))] + fn make_lazy_internal(&mut self, initializer: F, strategy: u8) -> Result<()> + where + F: Fn() -> R + 'static, + R: IntoZval + 'static, + { + // Wrap the Rust closure in a PHP-callable Closure + let closure = Closure::wrap(Box::new(initializer) as Box R>); + + // Convert the closure to a zval + let mut initializer_zv = Zval::new(); + closure.set_zval(&mut initializer_zv, false)?; + + // Initialize the fcc structure + let mut fcc: _zend_fcall_info_cache = unsafe { std::mem::zeroed() }; + + // Populate the fcc using zend_is_callable_ex + let is_callable = unsafe { + zend_is_callable_ex( + &raw mut initializer_zv, + ptr::null_mut(), + 0, + ptr::null_mut(), + &raw mut fcc, + ptr::null_mut(), + ) + }; + + if !is_callable { + return Err(Error::Callable); + } + + // Get the class entry + let ce = self.ce; + + // Make the object lazy + let result = unsafe { + zend_object_make_lazy(self, ce, &raw mut initializer_zv, &raw mut fcc, strategy) + }; + + if result.is_null() { + Err(Error::InvalidScope) + } else { + Ok(()) + } + } + /// Attempts to retrieve a reference to the object handlers. #[inline] unsafe fn handlers(&self) -> Result<&ZendObjectHandlers> { diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 3e70eb950..6ab381310 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,6 +5,9 @@ edition = "2024" publish = false license = "MIT OR Apache-2.0" +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(php84)'] } + [dependencies] ext-php-rs = { path = "../", default-features = false } diff --git a/tests/build.rs b/tests/build.rs new file mode 100644 index 000000000..a0b678542 --- /dev/null +++ b/tests/build.rs @@ -0,0 +1,88 @@ +//! Build script for the tests crate that detects PHP version and sets cfg flags. +//! +//! This mirrors the PHP version detection in ext-php-rs's build.rs to ensure +//! conditional compilation flags like `php82` are set correctly for the test code. + +use std::path::PathBuf; +use std::process::Command; + +/// Finds the location of an executable `name`. +fn find_executable(name: &str) -> Option { + const WHICH: &str = if cfg!(windows) { "where" } else { "which" }; + let cmd = Command::new(WHICH).arg(name).output().ok()?; + if cmd.status.success() { + let stdout = String::from_utf8_lossy(&cmd.stdout); + stdout.trim().lines().next().map(|l| l.trim().into()) + } else { + None + } +} + +/// Finds the location of the PHP executable. +fn find_php() -> Option { + // If path is given via env, it takes priority. + if let Some(path) = std::env::var_os("PHP").map(PathBuf::from) + && path.try_exists().unwrap_or(false) + { + return Some(path); + } + find_executable("php") +} + +/// Get PHP version as a (major, minor) tuple. +fn get_php_version() -> Option<(u32, u32)> { + let php = find_php()?; + let output = Command::new(&php) + .arg("-r") + .arg("echo PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;") + .output() + .ok()?; + + if !output.status.success() { + return None; + } + + let version_str = String::from_utf8_lossy(&output.stdout); + let parts: Vec<&str> = version_str.trim().split('.').collect(); + if parts.len() >= 2 { + let major = parts[0].parse().ok()?; + let minor = parts[1].parse().ok()?; + Some((major, minor)) + } else { + None + } +} + +fn main() { + // Declare check-cfg for all PHP version flags + println!("cargo::rustc-check-cfg=cfg(php80, php81, php82, php83, php84, php85)"); + + // Rerun if PHP environment changes + println!("cargo:rerun-if-env-changed=PHP"); + println!("cargo:rerun-if-env-changed=PATH"); + + let Some((major, minor)) = get_php_version() else { + eprintln!("Warning: Could not detect PHP version, DNF tests may not run"); + return; + }; + + // Set cumulative version flags (like ext-php-rs does) + // PHP 8.0 is baseline, no flag needed + if major >= 8 { + if minor >= 1 { + println!("cargo:rustc-cfg=php81"); + } + if minor >= 2 { + println!("cargo:rustc-cfg=php82"); + } + if minor >= 3 { + println!("cargo:rustc-cfg=php83"); + } + if minor >= 4 { + println!("cargo:rustc-cfg=php84"); + } + if minor >= 5 { + println!("cargo:rustc-cfg=php85"); + } + } +} diff --git a/tests/src/integration/class/class.php b/tests/src/integration/class/class.php index 13b98b962..207ae504c 100644 --- a/tests/src/integration/class/class.php +++ b/tests/src/integration/class/class.php @@ -122,3 +122,66 @@ // Test returning &Self (immutable reference) $selfRef = $builder2->getSelf(); assert($selfRef === $builder2, 'getSelf should return $this'); + +// Test lazy objects (PHP 8.4+) +if (PHP_VERSION_ID >= 80400) { + // Test with a regular (non-lazy) Rust object first + $regularObj = new TestLazyClass('regular'); + assert(test_is_lazy($regularObj) === false, 'Regular Rust object should not be lazy'); + assert(test_is_lazy_ghost($regularObj) === false, 'Regular Rust object should not be lazy ghost'); + assert(test_is_lazy_proxy($regularObj) === false, 'Regular Rust object should not be lazy proxy'); + assert(test_is_lazy_initialized($regularObj) === false, 'Regular Rust object lazy_initialized should be false'); + // PHP 8.4 lazy objects only work with user-defined PHP classes, not internal classes. + // Rust-defined classes are internal classes, so we test with a pure PHP class. + class PhpLazyTestClass { + public string $data = ''; + public bool $initialized = false; + + public function __construct(string $data) { + $this->data = $data; + $this->initialized = true; + } + } + + // Create a lazy ghost using PHP's Reflection API + $reflector = new ReflectionClass(PhpLazyTestClass::class); + $lazyGhost = $reflector->newLazyGhost(function (PhpLazyTestClass $obj) { + $obj->__construct('lazy initialized'); + }); + + // Verify lazy ghost introspection BEFORE initialization + assert(test_is_lazy($lazyGhost) === true, 'Lazy ghost should be lazy'); + assert(test_is_lazy_ghost($lazyGhost) === true, 'Lazy ghost should be identified as ghost'); + assert(test_is_lazy_proxy($lazyGhost) === false, 'Lazy ghost should not be identified as proxy'); + assert(test_is_lazy_initialized($lazyGhost) === false, 'Lazy ghost should not be initialized yet'); + + // Access a property to trigger initialization + $data = $lazyGhost->data; + assert($data === 'lazy initialized', 'Lazy ghost should be initialized with correct data'); + + // Verify lazy ghost introspection AFTER initialization + // Note: Initialized lazy ghosts become indistinguishable from regular objects (flags cleared) + assert(test_is_lazy($lazyGhost) === false, 'Initialized lazy ghost should no longer report as lazy'); + assert(test_is_lazy_initialized($lazyGhost) === false, 'Initialized ghost returns false (not lazy anymore)'); + + // Create a lazy proxy + $lazyProxy = $reflector->newLazyProxy(function (PhpLazyTestClass $obj) { + return new PhpLazyTestClass('proxy target'); + }); + + // Verify lazy proxy introspection BEFORE initialization + assert(test_is_lazy($lazyProxy) === true, 'Lazy proxy should be lazy'); + assert(test_is_lazy_ghost($lazyProxy) === false, 'Lazy proxy should not be identified as ghost'); + assert(test_is_lazy_proxy($lazyProxy) === true, 'Lazy proxy should be identified as proxy'); + assert(test_is_lazy_initialized($lazyProxy) === false, 'Lazy proxy should not be initialized yet'); + + // Trigger initialization + $proxyData = $lazyProxy->data; + assert($proxyData === 'proxy target', 'Lazy proxy should forward to real instance'); + + // Verify lazy proxy introspection AFTER initialization + // Note: Initialized lazy proxies still report as lazy (IS_OBJ_LAZY_PROXY stays set) + assert(test_is_lazy($lazyProxy) === true, 'Initialized lazy proxy should still report as lazy'); + assert(test_is_lazy_proxy($lazyProxy) === true, 'Initialized proxy should still be identified as proxy'); + assert(test_is_lazy_initialized($lazyProxy) === true, 'Lazy proxy should be initialized after property access'); +} diff --git a/tests/src/integration/class/mod.rs b/tests/src/integration/class/mod.rs index 6a547ca46..afb8b1f3b 100644 --- a/tests/src/integration/class/mod.rs +++ b/tests/src/integration/class/mod.rs @@ -7,6 +7,9 @@ use ext_php_rs::{ zend::ce, }; +#[cfg(php84)] +use ext_php_rs::types::ZendObject; + /// Doc comment /// Goes here #[php_class] @@ -232,6 +235,53 @@ impl TestStaticProps { } } +/// Test class for lazy object support (PHP 8.4+) +#[php_class] +pub struct TestLazyClass { + #[php(prop)] + pub data: String, + #[php(prop)] + pub initialized: bool, +} + +#[php_impl] +impl TestLazyClass { + pub fn __construct(data: String) -> Self { + Self { + data, + initialized: true, + } + } +} + +/// Check if a `ZendObject` is lazy +#[cfg(php84)] +#[php_function] +pub fn test_is_lazy(obj: &ZendObject) -> bool { + obj.is_lazy() +} + +/// Check if a `ZendObject` is a lazy ghost +#[cfg(php84)] +#[php_function] +pub fn test_is_lazy_ghost(obj: &ZendObject) -> bool { + obj.is_lazy_ghost() +} + +/// Check if a `ZendObject` is a lazy proxy +#[cfg(php84)] +#[php_function] +pub fn test_is_lazy_proxy(obj: &ZendObject) -> bool { + obj.is_lazy_proxy() +} + +/// Check if a lazy object has been initialized +#[cfg(php84)] +#[php_function] +pub fn test_is_lazy_initialized(obj: &ZendObject) -> bool { + obj.is_lazy_initialized() +} + /// Test class for returning $this (Issue #502) /// This demonstrates returning &mut Self from methods for fluent interfaces #[php_class] @@ -278,7 +328,7 @@ impl FluentBuilder { } pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { - builder + let builder = builder .class::() .class::() .class::() @@ -287,8 +337,18 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { .class::() .class::() .class::() + .class::() .function(wrap_function!(test_class)) - .function(wrap_function!(throw_exception)) + .function(wrap_function!(throw_exception)); + + #[cfg(php84)] + let builder = builder + .function(wrap_function!(test_is_lazy)) + .function(wrap_function!(test_is_lazy_ghost)) + .function(wrap_function!(test_is_lazy_proxy)) + .function(wrap_function!(test_is_lazy_initialized)); + + builder } #[cfg(test)] diff --git a/tools/update_lib_docs.sh b/tools/update_lib_docs.sh index 9c23ac61d..be810b98b 100755 --- a/tools/update_lib_docs.sh +++ b/tools/update_lib_docs.sh @@ -54,4 +54,4 @@ update_docs "enum" update_docs "interface" # Format to remove trailing whitespace -rustup run nightly rustfmt crates/macros/src/lib.rs +rustup run nightly rustfmt --edition 2024 crates/macros/src/lib.rs