Skip to content

Commit ee30283

Browse files
committed
feat(object): Lazy Ghost and Lazy Proxy
1 parent cab1c2c commit ee30283

File tree

10 files changed

+732
-4
lines changed

10 files changed

+732
-4
lines changed

allowed_bindings.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ bind! {
106106
zend_hash_str_update,
107107
zend_internal_arg_info,
108108
zend_is_callable,
109+
zend_is_callable_ex,
110+
zend_fcall_info_cache,
111+
_zend_fcall_info_cache,
109112
zend_is_identical,
110113
zend_is_iterable,
111114
zend_known_strings,
@@ -261,6 +264,17 @@ bind! {
261264
zend_std_get_properties,
262265
zend_std_has_property,
263266
zend_objects_new,
267+
zend_object_make_lazy,
268+
zend_lazy_object_init,
269+
zend_lazy_object_mark_as_initialized,
270+
zend_lazy_object_get_instance,
271+
zend_lazy_object_get_flags,
272+
zend_class_can_be_lazy,
273+
zend_lazy_object_flags_t,
274+
ZEND_LAZY_OBJECT_STRATEGY_GHOST,
275+
ZEND_LAZY_OBJECT_STRATEGY_PROXY,
276+
ZEND_LAZY_OBJECT_INITIALIZED,
277+
ZEND_LAZY_OBJECT_SKIP_INITIALIZATION_ON_SERIALIZE,
264278
zend_standard_class_def,
265279
zend_class_serialize_deny,
266280
zend_class_unserialize_deny,

crates/macros/src/function.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -972,7 +972,8 @@ fn expr_to_php_stub(expr: &Expr) -> String {
972972
}
973973
}
974974

975-
/// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
975+
/// Returns true if the given type is nullable in PHP (i.e., it's an
976+
/// `Option<T>`).
976977
///
977978
/// Note: Having a default value does NOT make a type nullable. A parameter with
978979
/// a default value is optional (can be omitted), but passing `null` explicitly

docsrs_bindings.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ pub const E_STRICT: u32 = 2048;
189189
pub const E_RECOVERABLE_ERROR: u32 = 4096;
190190
pub const E_DEPRECATED: u32 = 8192;
191191
pub const E_USER_DEPRECATED: u32 = 16384;
192+
pub const ZEND_LAZY_OBJECT_STRATEGY_PROXY: u32 = 1;
193+
pub const ZEND_LAZY_OBJECT_STRATEGY_GHOST: u32 = 2;
194+
pub const ZEND_LAZY_OBJECT_INITIALIZED: u32 = 4;
195+
pub const ZEND_LAZY_OBJECT_SKIP_INITIALIZATION_ON_SERIALIZE: u32 = 8;
192196
pub const ZEND_PROPERTY_ISSET: u32 = 0;
193197
pub const ZEND_PROPERTY_EXISTS: u32 = 2;
194198
pub const ZEND_ACC_PUBLIC: u32 = 1;
@@ -1211,13 +1215,38 @@ pub type zend_error_handling_t = ::std::os::raw::c_uint;
12111215
pub const zend_property_hook_kind_ZEND_PROPERTY_HOOK_GET: zend_property_hook_kind = 0;
12121216
pub const zend_property_hook_kind_ZEND_PROPERTY_HOOK_SET: zend_property_hook_kind = 1;
12131217
pub type zend_property_hook_kind = ::std::os::raw::c_uint;
1218+
pub type zend_lazy_object_flags_t = u8;
12141219
#[repr(C)]
12151220
pub struct _zend_lazy_objects_store {
12161221
pub infos: HashTable,
12171222
}
12181223
pub type zend_lazy_objects_store = _zend_lazy_objects_store;
12191224
pub type zend_property_info = _zend_property_info;
12201225
pub type zend_fcall_info_cache = _zend_fcall_info_cache;
1226+
unsafe extern "C" {
1227+
pub fn zend_class_can_be_lazy(ce: *const zend_class_entry) -> bool;
1228+
}
1229+
unsafe extern "C" {
1230+
pub fn zend_object_make_lazy(
1231+
obj: *mut zend_object,
1232+
class_type: *mut zend_class_entry,
1233+
initializer_zv: *mut zval,
1234+
initializer_fcc: *mut zend_fcall_info_cache,
1235+
flags: zend_lazy_object_flags_t,
1236+
) -> *mut zend_object;
1237+
}
1238+
unsafe extern "C" {
1239+
pub fn zend_lazy_object_init(obj: *mut zend_object) -> *mut zend_object;
1240+
}
1241+
unsafe extern "C" {
1242+
pub fn zend_lazy_object_mark_as_initialized(obj: *mut zend_object) -> *mut zend_object;
1243+
}
1244+
unsafe extern "C" {
1245+
pub fn zend_lazy_object_get_instance(obj: *mut zend_object) -> *mut zend_object;
1246+
}
1247+
unsafe extern "C" {
1248+
pub fn zend_lazy_object_get_flags(obj: *const zend_object) -> zend_lazy_object_flags_t;
1249+
}
12211250
pub type zend_object_read_property_t = ::std::option::Option<
12221251
unsafe extern "C" fn(
12231252
object: *mut zend_object,
@@ -2084,6 +2113,16 @@ unsafe extern "C" {
20842113
orig_class_entry: *mut zend_class_entry,
20852114
) -> *mut zend_class_entry;
20862115
}
2116+
unsafe extern "C" {
2117+
pub fn zend_is_callable_ex(
2118+
callable: *mut zval,
2119+
object: *mut zend_object,
2120+
check_flags: u32,
2121+
callable_name: *mut *mut zend_string,
2122+
fcc: *mut zend_fcall_info_cache,
2123+
error: *mut *mut ::std::os::raw::c_char,
2124+
) -> bool;
2125+
}
20872126
unsafe extern "C" {
20882127
pub fn zend_is_callable(
20892128
callable: *mut zval,

guide/src/types/object.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,136 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
7878
# fn main() {}
7979
```
8080

81+
## Lazy Objects (PHP 8.4+)
82+
83+
PHP 8.4 introduced lazy objects, which defer their initialization until their
84+
properties are first accessed. ext-php-rs provides APIs to introspect and create
85+
lazy objects from Rust.
86+
87+
### Lazy Object Types
88+
89+
There are two types of lazy objects:
90+
91+
- **Lazy Ghosts**: The ghost object itself becomes the real instance when
92+
initialized. After initialization, the ghost is indistinguishable from a
93+
regular object.
94+
95+
- **Lazy Proxies**: A proxy wraps a real instance that is created when first
96+
accessed. The proxy and real instance have different identities. After
97+
initialization, the proxy still reports as lazy.
98+
99+
### Introspection APIs
100+
101+
```rust,no_run
102+
# #![cfg_attr(windows, feature(abi_vectorcall))]
103+
# extern crate ext_php_rs;
104+
use ext_php_rs::{prelude::*, types::ZendObject};
105+
106+
#[php_function]
107+
pub fn check_lazy(obj: &ZendObject) -> String {
108+
if obj.is_lazy() {
109+
if obj.is_lazy_ghost() {
110+
if obj.is_lazy_initialized() {
111+
"Initialized lazy ghost".into()
112+
} else {
113+
"Uninitialized lazy ghost".into()
114+
}
115+
} else if obj.is_lazy_proxy() {
116+
if obj.is_lazy_initialized() {
117+
"Initialized lazy proxy".into()
118+
} else {
119+
"Uninitialized lazy proxy".into()
120+
}
121+
} else {
122+
"Unknown lazy type".into()
123+
}
124+
} else {
125+
"Not a lazy object".into()
126+
}
127+
}
128+
# fn main() {}
129+
```
130+
131+
Available introspection methods:
132+
133+
| Method | Description |
134+
|--------|-------------|
135+
| `is_lazy()` | Returns `true` if the object is lazy (ghost or proxy) |
136+
| `is_lazy_ghost()` | Returns `true` if the object is a lazy ghost |
137+
| `is_lazy_proxy()` | Returns `true` if the object is a lazy proxy |
138+
| `is_lazy_initialized()` | Returns `true` if the lazy object has been initialized |
139+
| `lazy_init()` | Triggers initialization of a lazy object |
140+
| `lazy_get_instance()` | For proxies, returns the real instance after initialization |
141+
142+
### Creating Lazy Objects from Rust
143+
144+
You can create lazy objects from Rust using `make_lazy_ghost()` and
145+
`make_lazy_proxy()`. These methods require the `closure` feature:
146+
147+
```toml
148+
[dependencies]
149+
ext-php-rs = { version = "0.15", features = ["closure"] }
150+
```
151+
152+
```rust,no_run
153+
# #![cfg_attr(windows, feature(abi_vectorcall))]
154+
# extern crate ext_php_rs;
155+
use ext_php_rs::{prelude::*, types::ZendObject, boxed::ZBox};
156+
157+
#[php_function]
158+
pub fn create_lazy_ghost(obj: &mut ZendObject) -> PhpResult<()> {
159+
let init_value = "initialized".to_string();
160+
obj.make_lazy_ghost(Box::new(move || {
161+
// Initialization logic - use captured state
162+
println!("Initializing with: {}", init_value);
163+
}) as Box<dyn Fn()>)?;
164+
Ok(())
165+
}
166+
167+
#[php_function]
168+
pub fn create_lazy_proxy(obj: &mut ZendObject) -> PhpResult<()> {
169+
obj.make_lazy_proxy(Box::new(|| {
170+
// Return the real instance
171+
Some(ZendObject::new_stdclass())
172+
}) as Box<dyn Fn() -> Option<ZBox<ZendObject>>>)?;
173+
Ok(())
174+
}
175+
# fn main() {}
176+
```
177+
178+
### Creating Lazy Objects from PHP
179+
180+
For full control over lazy object creation, use PHP's Reflection API:
181+
182+
```php
183+
<?php
184+
// Create a lazy ghost
185+
$reflector = new ReflectionClass(MyClass::class);
186+
$ghost = $reflector->newLazyGhost(function ($obj) {
187+
$obj->__construct('initialized');
188+
});
189+
190+
// Create a lazy proxy
191+
$proxy = $reflector->newLazyProxy(function ($obj) {
192+
return new MyClass('initialized');
193+
});
194+
```
195+
196+
### Limitations
197+
198+
- **PHP 8.4+ only**: Lazy objects are a PHP 8.4 feature and not available in
199+
earlier versions.
200+
201+
- **`closure` feature required**: The `make_lazy_ghost()` and `make_lazy_proxy()`
202+
methods require the `closure` feature to be enabled.
203+
204+
- **User-defined classes only**: PHP lazy objects only work with user-defined
205+
PHP classes, not internal classes. Since Rust-defined classes (using
206+
`#[php_class]`) are registered as internal classes, they cannot be made lazy.
207+
208+
- **Closure parameter access**: Due to Rust trait system limitations, the
209+
`make_lazy_ghost()` and `make_lazy_proxy()` closures don't receive the object
210+
being initialized as a parameter. Capture any needed initialization state in
211+
the closure itself.
212+
81213
[class object]: ./class_object.md

0 commit comments

Comments
 (0)