Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion crates/macros/src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`).
/// Returns true if the given type is nullable in PHP (i.e., it's an
/// `Option<T>`).
///
/// 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
Expand Down
33 changes: 33 additions & 0 deletions docsrs_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1211,13 +1215,32 @@ 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,
}
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,
Expand Down Expand Up @@ -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,
Expand Down
132 changes: 132 additions & 0 deletions guide/src/types/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Fn()>)?;
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<dyn Fn() -> Option<ZBox<ZendObject>>>)?;
Ok(())
}
# fn main() {}
```

### Creating Lazy Objects from PHP

For full control over lazy object creation, use PHP's Reflection API:

```php
<?php
// Create a lazy ghost
$reflector = new ReflectionClass(MyClass::class);
$ghost = $reflector->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
Loading