diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index b3fa7fc6b..577bfdc61 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -17,6 +17,7 @@ - [`bool`](./types/bool.md) - [`Vec`](./types/vec.md) - [`HashMap`](./types/hashmap.md) + - [`ZendHashTable`](./types/zend_hashtable.md) - [`Binary`](./types/binary.md) - [`BinarySlice`](./types/binary_slice.md) - [`Option`](./types/option.md) diff --git a/guide/src/types/zend_hashtable.md b/guide/src/types/zend_hashtable.md new file mode 100644 index 000000000..ae8c1eb70 --- /dev/null +++ b/guide/src/types/zend_hashtable.md @@ -0,0 +1,279 @@ +# `ZendHashTable` + +`ZendHashTable` is the internal representation of PHP arrays. While you can use +`Vec` and `HashMap` for most use cases (which are converted to/from +`ZendHashTable` automatically), working directly with `ZendHashTable` gives you +more control and avoids copying data when you need to manipulate PHP arrays +in-place. + +## When to use `ZendHashTable` directly + +- When you need to modify a PHP array in place without copying +- When working with arrays passed by reference +- When you need fine-grained control over array operations +- When implementing custom iterators or data structures + +## Basic Operations + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::prelude::*; +use ext_php_rs::types::ZendHashTable; +use ext_php_rs::boxed::ZBox; + +#[php_function] +pub fn create_array() -> ZBox { + let mut ht = ZendHashTable::new(); + + // Push values (auto-incrementing numeric keys) + ht.push("first").unwrap(); + ht.push("second").unwrap(); + + // Insert with string keys + ht.insert("name", "John").unwrap(); + ht.insert("age", 30i64).unwrap(); + + // Insert at specific numeric index + ht.insert_at_index(100, "at index 100").unwrap(); + + ht +} + +#[php_function] +pub fn read_array(arr: &ZendHashTable) { + // Get by string key + if let Some(name) = arr.get("name") { + println!("Name: {:?}", name.str()); + } + + // Get by numeric index + if let Some(first) = arr.get_index(0) { + println!("First: {:?}", first.str()); + } + + // Check length + println!("Length: {}", arr.len()); + println!("Is empty: {}", arr.is_empty()); + + // Iterate over key-value pairs + for (key, value) in arr.iter() { + println!("{}: {:?}", key, value); + } +} +# fn main() {} +``` + +## Entry API + +The Entry API provides an ergonomic way to handle hash table operations where +you need to conditionally insert or update values based on whether a key already +exists. This is similar to Rust's `std::collections::hash_map::Entry` API. + +### Basic Usage + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::prelude::*; +use ext_php_rs::types::ZendHashTable; +use ext_php_rs::boxed::ZBox; + +#[php_function] +pub fn entry_example() -> ZBox { + let mut ht = ZendHashTable::new(); + + // Insert a default value if the key doesn't exist + ht.entry("counter").or_insert(0i64).unwrap(); + + // Modify the value if it exists, using and_modify + ht.entry("counter") + .and_modify(|v| { + if let Some(n) = v.long() { + v.set_long(n + 1); + } + }) + .or_insert(0i64) + .unwrap(); + + // Use or_insert_with for lazy initialization + ht.entry("computed") + .or_insert_with(|| "computed value") + .unwrap(); + + // Works with numeric keys too + ht.entry(42i64).or_insert("value at index 42").unwrap(); + + ht +} +# fn main() {} +``` + +### Entry Variants + +The `entry()` method returns an `Entry` enum with two variants: + +- `Entry::Occupied` - The key exists in the hash table +- `Entry::Vacant` - The key does not exist + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::prelude::*; +use ext_php_rs::types::{ZendHashTable, Entry}; +use ext_php_rs::boxed::ZBox; + +#[php_function] +pub fn match_entry() -> ZBox { + let mut ht = ZendHashTable::new(); + ht.insert("existing", "value").unwrap(); + + // Pattern match on the entry + match ht.entry("existing") { + Entry::Occupied(entry) => { + println!("Key {:?} exists with value {:?}", + entry.key(), + entry.get().ok().and_then(|v| v.str())); + } + Entry::Vacant(entry) => { + println!("Key {:?} is vacant", entry.key()); + entry.insert("new value").unwrap(); + } + } + + ht +} +# fn main() {} +``` + +### Common Patterns + +#### Counting occurrences + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::prelude::*; +use ext_php_rs::types::ZendHashTable; +use ext_php_rs::boxed::ZBox; + +#[php_function] +pub fn count_words(words: Vec) -> ZBox { + let mut counts = ZendHashTable::new(); + + for word in words { + counts.entry(word.as_str()) + .and_modify(|v| { + if let Some(n) = v.long() { + v.set_long(n + 1); + } + }) + .or_insert(1i64) + .unwrap(); + } + + counts +} +# fn main() {} +``` + +#### Caching computed values + +This example demonstrates using `or_insert_with_key` for lazy computation: + +```rust,no_run +# extern crate ext_php_rs; +use ext_php_rs::types::ZendHashTable; + +fn expensive_computation(key: &str) -> String { + format!("computed_{}", key) +} + +fn get_or_compute(cache: &mut ZendHashTable, key: &str) -> String { + let value = cache.entry(key) + .or_insert_with_key(|k| expensive_computation(&k.to_string())) + .unwrap(); + + value.str().unwrap_or_default().to_string() +} +# fn main() {} +``` + +#### Updating existing values + +This example shows how to conditionally update a value only if the key exists: + +```rust,no_run +# extern crate ext_php_rs; +use ext_php_rs::types::{ZendHashTable, Entry}; + +fn update_if_exists(ht: &mut ZendHashTable, key: &str, new_value: &str) -> bool { + match ht.entry(key) { + Entry::Occupied(mut entry) => { + entry.insert(new_value).unwrap(); + true + } + Entry::Vacant(_) => false, + } +} +# fn main() {} +``` + +### Entry Methods Reference + +#### `Entry` methods + +| Method | Description | +|-------------------------|------------------------------------------------| +| `or_insert(default)` | Insert `default` if vacant, return `&mut Zval` | +| `or_insert_with(f)` | Insert result of `f()` if vacant | +| `or_insert_with_key(f)` | Insert result of `f(&key)` if vacant | +| `or_default()` | Insert default `Zval` (null) if vacant | +| `key()` | Get reference to the key | +| `and_modify(f)` | Modify value in place if occupied | + +#### `OccupiedEntry` methods + +| Method | Description | +|------------------|----------------------------------------------------| +| `key()` | Get reference to key | +| `get()` | Get reference to value | +| `get_mut()` | Get mutable reference to value | +| `into_mut()` | Convert to mutable reference with entry's lifetime | +| `insert(value)` | Replace value, returning old value | +| `remove()` | Remove and return value | +| `remove_entry()` | Remove and return key-value pair | + +#### `VacantEntry` methods + +| Method | Description | +|-----------------|-------------------------------------| +| `key()` | Get reference to key | +| `into_key()` | Take ownership of key | +| `insert(value)` | Insert value and return `&mut Zval` | + +## PHP Example + +```php + string(5) "first" +// [1]=> string(6) "second" +// ["name"]=> string(4) "John" +// ["age"]=> int(30) +// [100]=> string(12) "at index 100" +// } + +// Count words +$counts = count_words(['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']); +var_dump($counts); +// array(3) { +// ["apple"]=> int(3) +// ["banana"]=> int(2) +// ["cherry"]=> int(1) +// } +``` diff --git a/src/types/array/entry.rs b/src/types/array/entry.rs new file mode 100644 index 000000000..5819b0330 --- /dev/null +++ b/src/types/array/entry.rs @@ -0,0 +1,750 @@ +//! Entry API for [`ZendHashTable`], similar to Rust's `std::collections::hash_map::Entry`. +//! +//! This module provides an ergonomic API for working with entries in a PHP +//! hashtable, allowing conditional insertion or modification based on whether +//! a key already exists. +//! +//! # Examples +//! +//! ```no_run +//! use ext_php_rs::types::ZendHashTable; +//! +//! let mut ht = ZendHashTable::new(); +//! +//! // Insert a default value if the key doesn't exist +//! ht.entry("counter").or_insert(0i64); +//! +//! // Modify the value if it exists +//! ht.entry("counter").and_modify(|v| { +//! if let Some(n) = v.long() { +//! v.set_long(n + 1); +//! } +//! }); +//! +//! // Or use or_insert_with for lazy initialization +//! ht.entry("computed").or_insert_with(|| "computed value"); +//! ``` + +use std::ffi::CString; + +use super::{ArrayKey, ZendHashTable}; +use crate::{ + convert::IntoZval, + error::{Error, Result}, + ffi::{ + zend_hash_index_find, zend_hash_index_update, zend_hash_str_find, zend_hash_str_update, + zend_ulong, + }, + types::Zval, +}; + +/// A view into a single entry in a [`ZendHashTable`], which may either be vacant or +/// occupied. +/// +/// This enum is constructed from the [`entry`] method on [`ZendHashTable`]. +/// +/// [`entry`]: ZendHashTable::entry +pub enum Entry<'a, 'k> { + /// An occupied entry. + Occupied(OccupiedEntry<'a, 'k>), + /// A vacant entry. + Vacant(VacantEntry<'a, 'k>), +} + +/// A view into an occupied entry in a [`ZendHashTable`]. +/// +/// It is part of the [`Entry`] enum. +pub struct OccupiedEntry<'a, 'k> { + ht: &'a mut ZendHashTable, + key: ArrayKey<'k>, +} + +/// A view into a vacant entry in a [`ZendHashTable`]. +/// +/// It is part of the [`Entry`] enum. +pub struct VacantEntry<'a, 'k> { + ht: &'a mut ZendHashTable, + key: ArrayKey<'k>, +} + +impl<'a, 'k> Entry<'a, 'k> { + /// Ensures a value is in the entry by inserting the default if empty, and + /// returns a mutable reference to the value in the entry. + /// + /// # Parameters + /// + /// * `default` - The default value to insert if the entry is vacant. + /// + /// # Returns + /// + /// A result containing a mutable reference to the value, or an error if + /// the insertion failed. + /// + /// # Errors + /// + /// Returns an error if the value conversion to [`Zval`] fails or if + /// the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.entry("key").or_insert("default value"); + /// assert_eq!(ht.get("key").and_then(|v| v.str()), Some("default value")); + /// ``` + pub fn or_insert(self, default: V) -> Result<&'a mut Zval> { + match self { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => entry.insert(default), + } + } + + /// Ensures a value is in the entry by inserting the result of the default + /// function if empty, and returns a mutable reference to the value in the + /// entry. + /// + /// # Parameters + /// + /// * `default` - A function that returns the default value to insert. + /// + /// # Returns + /// + /// A result containing a mutable reference to the value, or an error if + /// the insertion failed. + /// + /// # Errors + /// + /// Returns an error if the value conversion to [`Zval`] fails or if + /// the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.entry("key").or_insert_with(|| "computed value"); + /// ``` + pub fn or_insert_with V>(self, default: F) -> Result<&'a mut Zval> { + match self { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => entry.insert(default()), + } + } + + /// Ensures a value is in the entry by inserting the result of the default + /// function if empty. The function receives a reference to the key. + /// + /// # Parameters + /// + /// * `default` - A function that takes the key and returns the default value. + /// + /// # Returns + /// + /// A result containing a mutable reference to the value, or an error if + /// the insertion failed. + /// + /// # Errors + /// + /// Returns an error if the value conversion to [`Zval`] fails or if + /// the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.entry("key").or_insert_with_key(|k| format!("value for {}", k)); + /// ``` + pub fn or_insert_with_key) -> V>( + self, + default: F, + ) -> Result<&'a mut Zval> { + match self { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let value = default(entry.key()); + entry.insert(value) + } + } + } + + /// Returns a reference to this entry's key. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::{ZendHashTable, ArrayKey}; + /// + /// let mut ht = ZendHashTable::new(); + /// assert_eq!(ht.entry("key").key(), &ArrayKey::Str("key")); + /// ``` + #[must_use] + pub fn key(&self) -> &ArrayKey<'k> { + match self { + Entry::Occupied(entry) => entry.key(), + Entry::Vacant(entry) => entry.key(), + } + } + + /// Provides in-place mutable access to an occupied entry before any + /// potential inserts into the map. + /// + /// # Parameters + /// + /// * `f` - A function that modifies the value in place. + /// + /// # Returns + /// + /// The entry, allowing for method chaining. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.insert("counter", 0i64); + /// + /// ht.entry("counter") + /// .and_modify(|v| { + /// if let Some(n) = v.long() { + /// v.set_long(n + 1); + /// } + /// }) + /// .or_insert(0i64); + /// ``` + #[must_use] + pub fn and_modify(self, f: F) -> Self { + match self { + Entry::Occupied(mut entry) => { + if let Ok(value) = entry.get_mut() { + f(value); + } + Entry::Occupied(entry) + } + Entry::Vacant(entry) => Entry::Vacant(entry), + } + } +} + +impl<'a, 'k> Entry<'a, 'k> +where + 'k: 'a, +{ + /// Ensures a value is in the entry by inserting the default value if empty, + /// and returns a mutable reference to the value in the entry. + /// + /// This is a convenience method that uses `Default::default()` as the + /// default value. + /// + /// # Returns + /// + /// A result containing a mutable reference to the value, or an error if + /// the insertion failed. + /// + /// # Errors + /// + /// Returns an error if the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// // Inserts a null Zval if the key doesn't exist + /// ht.entry("key").or_default(); + /// ``` + pub fn or_default(self) -> Result<&'a mut Zval> { + match self { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => entry.insert(Zval::new()), + } + } +} + +impl<'a, 'k> OccupiedEntry<'a, 'k> { + /// Creates a new occupied entry. + pub(super) fn new(ht: &'a mut ZendHashTable, key: ArrayKey<'k>) -> Self { + Self { ht, key } + } + + /// Gets a reference to the key in the entry. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::{ZendHashTable, ArrayKey}; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.insert("key", "value"); + /// + /// if let ext_php_rs::types::array::Entry::Occupied(entry) = ht.entry("key") { + /// assert_eq!(entry.key(), &ArrayKey::Str("key")); + /// } + /// ``` + #[must_use] + pub fn key(&self) -> &ArrayKey<'k> { + &self.key + } + + /// Gets a reference to the value in the entry. + /// + /// # Returns + /// + /// A result containing a reference to the value, or an error if + /// the key conversion failed. + /// + /// # Errors + /// + /// Returns an error if the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.insert("key", "value"); + /// + /// if let ext_php_rs::types::array::Entry::Occupied(entry) = ht.entry("key") { + /// assert_eq!(entry.get().ok().and_then(|v| v.str()), Some("value")); + /// } + /// ``` + pub fn get(&self) -> Result<&Zval> { + get_value(&self.key, self.ht).ok_or(Error::InvalidCString) + } + + /// Gets a mutable reference to the value in the entry. + /// + /// # Returns + /// + /// A result containing a mutable reference to the value, or an error if + /// the key conversion failed. + /// + /// # Errors + /// + /// Returns an error if the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.insert("counter", 0i64); + /// + /// if let ext_php_rs::types::array::Entry::Occupied(mut entry) = ht.entry("counter") { + /// if let Ok(v) = entry.get_mut() { + /// if let Some(n) = v.long() { + /// v.set_long(n + 1); + /// } + /// } + /// } + /// ``` + pub fn get_mut(&mut self) -> Result<&mut Zval> { + get_value_mut(&self.key, self.ht).ok_or(Error::InvalidCString) + } + + /// Converts the entry into a mutable reference to the value. + /// + /// If you need multiple references to the `OccupiedEntry`, see [`get_mut`]. + /// + /// [`get_mut`]: OccupiedEntry::get_mut + /// + /// # Returns + /// + /// A result containing a mutable reference to the value with the entry's + /// lifetime, or an error if the key conversion failed. + /// + /// # Errors + /// + /// Returns an error if the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.insert("key", "value"); + /// + /// if let ext_php_rs::types::array::Entry::Occupied(entry) = ht.entry("key") { + /// let value = entry.into_mut().unwrap(); + /// // value has the lifetime of the hashtable borrow + /// } + /// ``` + pub fn into_mut(self) -> Result<&'a mut Zval> { + get_value_mut(&self.key, self.ht).ok_or(Error::InvalidCString) + } + + /// Sets the value of the entry, returning the old value. + /// + /// # Parameters + /// + /// * `value` - The new value to set. + /// + /// # Returns + /// + /// A result containing the old value (shallow cloned), or an error if the + /// insertion failed. + /// + /// # Errors + /// + /// Returns an error if the value conversion to [`Zval`] fails or if + /// the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.insert("key", "old"); + /// + /// if let ext_php_rs::types::array::Entry::Occupied(mut entry) = ht.entry("key") { + /// let old = entry.insert("new").unwrap(); + /// assert_eq!(old.str(), Some("old")); + /// } + /// ``` + pub fn insert(&mut self, value: V) -> Result { + let old = self.get()?.shallow_clone(); + insert_value(&self.key, self.ht, value)?; + Ok(old) + } + + /// Takes ownership of the key and value from the entry, removing it from + /// the hashtable. + /// + /// # Returns + /// + /// A result containing a tuple of the key and the removed value (shallow + /// cloned), or an error if the removal failed. + /// + /// # Errors + /// + /// Returns an error if the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.insert("key", "value"); + /// + /// if let ext_php_rs::types::array::Entry::Occupied(entry) = ht.entry("key") { + /// let (key, value) = entry.remove_entry().unwrap(); + /// assert!(ht.get("key").is_none()); + /// } + /// ``` + pub fn remove_entry(self) -> Result<(ArrayKey<'k>, Zval)> { + let value = self.get()?.shallow_clone(); + self.ht.remove(self.key.clone()); + Ok((self.key, value)) + } + + /// Removes the value from the entry, returning it. + /// + /// # Returns + /// + /// A result containing the removed value (shallow cloned), or an error if + /// the removal failed. + /// + /// # Errors + /// + /// Returns an error if the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// ht.insert("key", "value"); + /// + /// if let ext_php_rs::types::array::Entry::Occupied(entry) = ht.entry("key") { + /// let value = entry.remove().unwrap(); + /// assert_eq!(value.str(), Some("value")); + /// } + /// ``` + pub fn remove(self) -> Result { + let value = self.get()?.shallow_clone(); + self.ht.remove(self.key); + Ok(value) + } +} + +impl<'a, 'k> VacantEntry<'a, 'k> { + /// Creates a new vacant entry. + pub(super) fn new(ht: &'a mut ZendHashTable, key: ArrayKey<'k>) -> Self { + Self { ht, key } + } + + /// Gets a reference to the key that would be used when inserting a value + /// through the `VacantEntry`. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::{ZendHashTable, ArrayKey}; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// if let ext_php_rs::types::array::Entry::Vacant(entry) = ht.entry("key") { + /// assert_eq!(entry.key(), &ArrayKey::Str("key")); + /// } + /// ``` + #[must_use] + pub fn key(&self) -> &ArrayKey<'k> { + &self.key + } + + /// Take ownership of the key. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::{ZendHashTable, ArrayKey}; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// if let ext_php_rs::types::array::Entry::Vacant(entry) = ht.entry("key") { + /// let key = entry.into_key(); + /// assert_eq!(key, ArrayKey::Str("key")); + /// } + /// ``` + #[must_use] + pub fn into_key(self) -> ArrayKey<'k> { + self.key + } + + /// Sets the value of the entry with the `VacantEntry`'s key, and returns + /// a mutable reference to it. + /// + /// # Parameters + /// + /// * `value` - The value to insert. + /// + /// # Returns + /// + /// A result containing a mutable reference to the inserted value, or an + /// error if the insertion failed. + /// + /// # Errors + /// + /// Returns an error if the value conversion to [`Zval`] fails or if + /// the key contains a null byte (for string keys). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// if let ext_php_rs::types::array::Entry::Vacant(entry) = ht.entry("key") { + /// entry.insert("value"); + /// } + /// assert_eq!(ht.get("key").and_then(|v| v.str()), Some("value")); + /// ``` + pub fn insert(self, value: V) -> Result<&'a mut Zval> { + insert_value(&self.key, self.ht, value)?; + get_value_mut(&self.key, self.ht).ok_or(Error::InvalidCString) + } +} + +/// Helper function to get a value from the hashtable by key. +fn get_value<'a>(key: &ArrayKey<'_>, ht: &'a ZendHashTable) -> Option<&'a Zval> { + match key { + ArrayKey::Long(index) => unsafe { + #[allow(clippy::cast_sign_loss)] + zend_hash_index_find(ht, *index as zend_ulong).as_ref() + }, + ArrayKey::String(key) => unsafe { + zend_hash_str_find(ht, CString::new(key.as_str()).ok()?.as_ptr(), key.len()).as_ref() + }, + ArrayKey::Str(key) => unsafe { + zend_hash_str_find(ht, CString::new(*key).ok()?.as_ptr(), key.len()).as_ref() + }, + } +} + +/// Helper function to get a mutable value from the hashtable by key. +fn get_value_mut<'a>(key: &ArrayKey<'_>, ht: &'a mut ZendHashTable) -> Option<&'a mut Zval> { + match key { + ArrayKey::Long(index) => unsafe { + #[allow(clippy::cast_sign_loss)] + zend_hash_index_find(ht, *index as zend_ulong).as_mut() + }, + ArrayKey::String(key) => unsafe { + zend_hash_str_find( + ht, + CString::new(key.as_str()).ok()?.as_ptr(), + key.len() as _, + ) + .as_mut() + }, + ArrayKey::Str(key) => unsafe { + zend_hash_str_find(ht, CString::new(*key).ok()?.as_ptr(), key.len() as _).as_mut() + }, + } +} + +/// Helper function to insert a value into the hashtable by key. +fn insert_value(key: &ArrayKey<'_>, ht: &mut ZendHashTable, value: V) -> Result<()> { + let mut val = value.into_zval(false)?; + match key { + ArrayKey::Long(index) => { + unsafe { + #[allow(clippy::cast_sign_loss)] + zend_hash_index_update(ht, *index as zend_ulong, &raw mut val) + }; + } + ArrayKey::String(key) => { + unsafe { + zend_hash_str_update( + ht, + CString::new(key.as_str())?.as_ptr(), + key.len(), + &raw mut val, + ) + }; + } + ArrayKey::Str(key) => { + unsafe { + zend_hash_str_update(ht, CString::new(*key)?.as_ptr(), key.len(), &raw mut val) + }; + } + } + val.release(); + Ok(()) +} + +#[cfg(test)] +#[cfg(feature = "embed")] +mod tests { + use super::*; + use crate::embed::Embed; + + #[test] + fn test_entry_or_insert() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + + // Insert into vacant entry + let result = ht.entry("key").or_insert("value"); + assert!(result.is_ok()); + assert_eq!(ht.get("key").and_then(|v| v.str()), Some("value")); + + // Entry already exists, should return existing value + let result = ht.entry("key").or_insert("other"); + assert!(result.is_ok()); + assert_eq!(ht.get("key").and_then(|v| v.str()), Some("value")); + }); + } + + #[test] + fn test_entry_or_insert_with() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + + let result = ht.entry("key").or_insert_with(|| "computed"); + assert!(result.is_ok()); + assert_eq!(ht.get("key").and_then(|v| v.str()), Some("computed")); + }); + } + + #[test] + fn test_entry_and_modify() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + let _ = ht.insert("counter", 5i64); + + let result = ht + .entry("counter") + .and_modify(|v| { + if let Some(n) = v.long() { + v.set_long(n + 1); + } + }) + .or_insert(0i64); + + assert!(result.is_ok()); + assert_eq!(ht.get("counter").and_then(Zval::long), Some(6)); + }); + } + + #[test] + fn test_entry_and_modify_vacant() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + + // and_modify on vacant entry should be a no-op + let result = ht + .entry("key") + .and_modify(|v| { + v.set_long(100); + }) + .or_insert(42i64); + + assert!(result.is_ok()); + assert_eq!(ht.get("key").and_then(Zval::long), Some(42)); + }); + } + + #[test] + fn test_occupied_entry_insert() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + let _ = ht.insert("key", "old"); + + if let Entry::Occupied(mut entry) = ht.entry("key") { + let old = entry.insert("new").expect("insert should succeed"); + assert_eq!(old.str(), Some("old")); + } + assert_eq!(ht.get("key").and_then(|v| v.str()), Some("new")); + }); + } + + #[test] + fn test_occupied_entry_remove() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + let _ = ht.insert("key", "value"); + + if let Entry::Occupied(entry) = ht.entry("key") { + let value = entry.remove().expect("remove should succeed"); + assert_eq!(value.str(), Some("value")); + } + assert!(ht.get("key").is_none()); + }); + } + + #[test] + fn test_entry_with_numeric_key() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + + let result = ht.entry(42i64).or_insert("value"); + assert!(result.is_ok()); + assert_eq!(ht.get_index(42).and_then(|v| v.str()), Some("value")); + }); + } + + #[test] + fn test_vacant_entry_into_key() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + + if let Entry::Vacant(entry) = ht.entry("my_key") { + let key = entry.into_key(); + assert_eq!(key, ArrayKey::Str("my_key")); + } + }); + } +} diff --git a/src/types/array/iterators.rs b/src/types/array/iterators.rs index cd2ff0a16..d19918de1 100644 --- a/src/types/array/iterators.rs +++ b/src/types/array/iterators.rs @@ -34,6 +34,11 @@ impl<'a> Iter<'a> { /// # Parameters /// /// * `ht` - The hashtable to iterate. + /// + /// # Panics + /// + /// Panics if the hashtable length exceeds `i64::MAX`. + #[must_use] pub fn new(ht: &'a ZendHashTable) -> Self { let end_num: i64 = ht .len() @@ -54,6 +59,9 @@ impl<'a> Iter<'a> { } } + /// Advances the iterator and returns the next key-value pair as raw Zvals. + /// + /// Returns `None` when iteration is finished. pub fn next_zval(&mut self) -> Option<(Zval, &'a Zval)> { if self.current_num >= self.end_num { return None; @@ -225,6 +233,7 @@ impl<'a> Values<'a> { /// # Parameters /// /// * `ht` - The hashtable to iterate. + #[must_use] pub fn new(ht: &'a ZendHashTable) -> Self { Self(Iter::new(ht)) } diff --git a/src/types/array/mod.rs b/src/types/array/mod.rs index d19bcdebb..82d39e0bb 100644 --- a/src/types/array/mod.rs +++ b/src/types/array/mod.rs @@ -19,9 +19,11 @@ use crate::{ mod array_key; mod conversions; +mod entry; mod iterators; pub use array_key::ArrayKey; +pub use entry::{Entry, OccupiedEntry, VacantEntry}; pub use iterators::{Iter, Values}; /// A PHP hashtable. @@ -648,6 +650,99 @@ impl ZendHashTable { pub fn iter(&self) -> Iter<'_> { self.into_iter() } + + /// Gets the given key's corresponding entry in the hashtable for in-place + /// manipulation. + /// + /// This API is similar to Rust's [`std::collections::hash_map::HashMap::entry`]. + /// + /// # Parameters + /// + /// * `key` - The key to look up in the hashtable. + /// + /// # Returns + /// + /// An `Entry` enum that can be used to insert or modify the value at + /// the given key. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// // Insert a default value if the key doesn't exist + /// ht.entry("counter").or_insert(0i64); + /// + /// // Modify the value if it exists + /// ht.entry("counter").and_modify(|v| { + /// if let Some(n) = v.long() { + /// v.set_long(n + 1); + /// } + /// }); + /// + /// // Use or_insert_with for lazy initialization + /// ht.entry("computed").or_insert_with(|| "computed value"); + /// + /// // Works with numeric keys too + /// ht.entry(42i64).or_insert("value at index 42"); + /// ``` + pub fn entry<'a, 'k, K>(&'a mut self, key: K) -> Entry<'a, 'k> + where + K: Into>, + { + let key = key.into(); + if self.has_key(&key) { + Entry::Occupied(entry::OccupiedEntry::new(self, key)) + } else { + Entry::Vacant(entry::VacantEntry::new(self, key)) + } + } + + /// Checks if a key exists in the hash table. + /// + /// # Parameters + /// + /// * `key` - The key to check for in the hash table. + /// + /// # Returns + /// + /// * `true` - The key exists in the hash table. + /// * `false` - The key does not exist in the hash table. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::{ZendHashTable, ArrayKey}; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.insert("test", "hello world"); + /// assert!(ht.has_key(&ArrayKey::from("test"))); + /// assert!(!ht.has_key(&ArrayKey::from("missing"))); + /// ``` + #[must_use] + pub fn has_key(&self, key: &ArrayKey<'_>) -> bool { + match key { + ArrayKey::Long(index) => unsafe { + #[allow(clippy::cast_sign_loss)] + !zend_hash_index_find(self, *index as zend_ulong).is_null() + }, + ArrayKey::String(key) => { + let Ok(cstr) = CString::new(key.as_str()) else { + return false; + }; + unsafe { !zend_hash_str_find(self, cstr.as_ptr(), key.len() as _).is_null() } + } + ArrayKey::Str(key) => { + let Ok(cstr) = CString::new(*key) else { + return false; + }; + unsafe { !zend_hash_str_find(self, cstr.as_ptr(), key.len() as _).is_null() } + } + } + } } unsafe impl ZBoxable for ZendHashTable { @@ -711,3 +806,47 @@ impl<'a> FromZval<'a> for &'a ZendHashTable { zval.array() } } + +#[cfg(test)] +#[cfg(feature = "embed")] +mod tests { + use super::*; + use crate::embed::Embed; + + #[test] + fn test_has_key_string() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + let _ = ht.insert("test", "value"); + + assert!(ht.has_key(&ArrayKey::from("test"))); + assert!(!ht.has_key(&ArrayKey::from("missing"))); + }); + } + + #[test] + fn test_has_key_long() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + let _ = ht.push(42i64); + + assert!(ht.has_key(&ArrayKey::Long(0))); + assert!(!ht.has_key(&ArrayKey::Long(1))); + }); + } + + #[test] + fn test_has_key_str_ref() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + let _ = ht.insert("hello", "world"); + + let key = ArrayKey::Str("hello"); + assert!(ht.has_key(&key)); + // Key is still usable after has_key (no clone needed) + assert!(ht.has_key(&key)); + + assert!(!ht.has_key(&ArrayKey::Str("missing"))); + }); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index ffbf52bf3..0b3ef42b9 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,7 +3,7 @@ //! Generally, it is easier to work directly with Rust types, converting into //! these PHP types when required. -mod array; +pub mod array; mod callable; mod class_object; mod iterable; @@ -13,7 +13,7 @@ mod object; mod string; mod zval; -pub use array::{ArrayKey, ZendHashTable}; +pub use array::{ArrayKey, Entry, OccupiedEntry, VacantEntry, ZendHashTable}; pub use callable::ZendCallable; pub use class_object::ZendClassObject; pub use iterable::Iterable;