• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

extphprs / ext-php-rs / 26565713723

28 May 2026 09:11AM UTC coverage: 66.146% (-0.06%) from 66.207%
26565713723

Pull #743

github

web-flow
Merge 7dbb5b991 into cd7df3e47
Pull Request #743: feat: Support using ZendStr as ZendHashTable keys

36 of 68 new or added lines in 5 files covered. (52.94%)

1 existing line in 1 file now uncovered.

8685 of 13130 relevant lines covered (66.15%)

33.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.7
/src/types/array/mod.rs
1
//! Represents an array in PHP. As all arrays in PHP are associative arrays,
2
//! they are represented by hash tables.
3

4
use std::{convert::TryFrom, fmt::Debug, ptr};
5

6
use crate::{
7
    boxed::{ZBox, ZBoxable},
8
    convert::{FromZval, FromZvalMut, IntoZval},
9
    error::Result,
10
    ffi::zend_ulong,
11
    ffi::{
12
        _zend_new_array, zend_array_count, zend_array_destroy, zend_array_dup, zend_empty_array,
13
        zend_hash_clean, zend_hash_del, zend_hash_find, zend_hash_index_del, zend_hash_index_find,
14
        zend_hash_index_update, zend_hash_next_index_insert, zend_hash_str_del, zend_hash_str_find,
15
        zend_hash_str_update, zend_hash_update, GC_FLAGS_MASK, GC_FLAGS_SHIFT, HT_MIN_SIZE,
16
    },
17
    flags::{DataType, ZvalTypeFlags},
18
    types::Zval,
19
};
20

21
mod array_key;
22
mod conversions;
23
mod entry;
24
mod iterators;
25

26
pub use array_key::ArrayKey;
27
pub use entry::{Entry, OccupiedEntry, VacantEntry};
28
pub use iterators::{Iter, Values};
29

30
/// A PHP hashtable.
31
///
32
/// In PHP, arrays are represented as hashtables. This allows you to push values
33
/// onto the end of the array like a vector, while also allowing you to insert
34
/// at arbitrary string key indexes.
35
///
36
/// A PHP hashtable stores values as [`Zval`]s. This allows you to insert
37
/// different types into the same hashtable. Types must implement [`IntoZval`]
38
/// to be able to be inserted into the hashtable.
39
///
40
/// # Examples
41
///
42
/// ```no_run
43
/// use ext_php_rs::types::ZendHashTable;
44
///
45
/// let mut ht = ZendHashTable::new();
46
/// ht.push(1);
47
/// ht.push("Hello, world!");
48
/// ht.insert("Like", "Hashtable");
49
///
50
/// assert_eq!(ht.len(), 3);
51
/// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(1));
52
/// ```
53
pub type ZendHashTable = crate::ffi::HashTable;
54

55
// Clippy complains about there being no `is_empty` function when implementing
56
// on the alias `ZendStr` :( <https://github.com/rust-lang/rust-clippy/issues/7702>
57
#[allow(clippy::len_without_is_empty)]
58
impl ZendHashTable {
59
    /// Creates a new, empty, PHP hashtable, returned inside a [`ZBox`].
60
    ///
61
    /// # Example
62
    ///
63
    /// ```no_run
64
    /// use ext_php_rs::types::ZendHashTable;
65
    ///
66
    /// let ht = ZendHashTable::new();
67
    /// ```
68
    ///
69
    /// # Panics
70
    ///
71
    /// Panics if memory for the hashtable could not be allocated.
72
    #[must_use]
73
    pub fn new() -> ZBox<Self> {
59✔
74
        Self::with_capacity(HT_MIN_SIZE)
59✔
75
    }
59✔
76

77
    /// Creates a new, empty, PHP hashtable with an initial size, returned
78
    /// inside a [`ZBox`].
79
    ///
80
    /// # Parameters
81
    ///
82
    /// * `size` - The size to initialize the array with.
83
    ///
84
    /// # Example
85
    ///
86
    /// ```no_run
87
    /// use ext_php_rs::types::ZendHashTable;
88
    ///
89
    /// let ht = ZendHashTable::with_capacity(10);
90
    /// ```
91
    ///
92
    /// # Panics
93
    ///
94
    /// Panics if memory for the hashtable could not be allocated.
95
    #[must_use]
96
    pub fn with_capacity(size: u32) -> ZBox<Self> {
89✔
97
        unsafe {
98
            // SAFETY: PHP allocator handles the creation of the array.
99
            #[allow(clippy::used_underscore_items)]
100
            let ptr = _zend_new_array(size);
89✔
101

102
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
103
            ZBox::from_raw(
89✔
104
                ptr.as_mut()
89✔
105
                    .expect("Failed to allocate memory for hashtable"),
89✔
106
            )
107
        }
108
    }
89✔
109

110
    /// Returns the current number of elements in the array.
111
    ///
112
    /// # Example
113
    ///
114
    /// ```no_run
115
    /// use ext_php_rs::types::ZendHashTable;
116
    ///
117
    /// let mut ht = ZendHashTable::new();
118
    ///
119
    /// ht.push(1);
120
    /// ht.push("Hello, world");
121
    ///
122
    /// assert_eq!(ht.len(), 2);
123
    /// ```
124
    #[must_use]
125
    pub fn len(&self) -> usize {
105✔
126
        unsafe { zend_array_count(ptr::from_ref(self).cast_mut()) as usize }
105✔
127
    }
105✔
128

129
    /// Returns whether the hash table is empty.
130
    ///
131
    /// # Example
132
    ///
133
    /// ```no_run
134
    /// use ext_php_rs::types::ZendHashTable;
135
    ///
136
    /// let mut ht = ZendHashTable::new();
137
    ///
138
    /// assert_eq!(ht.is_empty(), true);
139
    ///
140
    /// ht.push(1);
141
    /// ht.push("Hello, world");
142
    ///
143
    /// assert_eq!(ht.is_empty(), false);
144
    /// ```
145
    #[must_use]
146
    pub fn is_empty(&self) -> bool {
×
147
        self.len() == 0
×
148
    }
×
149

150
    /// Clears the hash table, removing all values.
151
    ///
152
    /// # Example
153
    ///
154
    /// ```no_run
155
    /// use ext_php_rs::types::ZendHashTable;
156
    ///
157
    /// let mut ht = ZendHashTable::new();
158
    ///
159
    /// ht.insert("test", "hello world");
160
    /// assert_eq!(ht.is_empty(), false);
161
    ///
162
    /// ht.clear();
163
    /// assert_eq!(ht.is_empty(), true);
164
    /// ```
165
    pub fn clear(&mut self) {
×
166
        unsafe { zend_hash_clean(self) }
×
167
    }
×
168

169
    /// Attempts to retrieve a value from the hash table with a string key.
170
    ///
171
    /// # Parameters
172
    ///
173
    /// * `key` - The key to search for in the hash table.
174
    ///
175
    /// # Returns
176
    ///
177
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
178
    ///   table.
179
    /// * `None` - No value at the given position was found.
180
    ///
181
    /// # Example
182
    ///
183
    /// ```no_run
184
    /// use ext_php_rs::types::ZendHashTable;
185
    ///
186
    /// let mut ht = ZendHashTable::new();
187
    ///
188
    /// ht.insert("test", "hello world");
189
    /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
190
    /// ```
191
    #[must_use]
192
    pub fn get<'a, K>(&self, key: K) -> Option<&Zval>
64✔
193
    where
64✔
194
        K: Into<ArrayKey<'a>>,
64✔
195
    {
196
        match key.into() {
64✔
197
            ArrayKey::Long(index) => unsafe {
32✔
198
                #[allow(clippy::cast_sign_loss)]
199
                zend_hash_index_find(self, index as zend_ulong).as_ref()
32✔
200
            },
201
            ArrayKey::String(key) => unsafe {
×
202
                zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_ref()
×
203
            },
204
            ArrayKey::Str(key) => unsafe {
31✔
205
                zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_ref()
31✔
206
            },
207
            ArrayKey::ZendString(key) => unsafe {
1✔
208
                zend_hash_find(self, key.as_ptr().cast_mut()).as_ref()
1✔
209
            },
210
        }
211
    }
64✔
212

213
    /// Attempts to retrieve a value from the hash table with a string key.
214
    ///
215
    /// # Parameters
216
    ///
217
    /// * `key` - The key to search for in the hash table.
218
    ///
219
    /// # Returns
220
    ///
221
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
222
    ///   table.
223
    /// * `None` - No value at the given position was found.
224
    ///
225
    /// # Example
226
    ///
227
    /// ```no_run
228
    /// use ext_php_rs::types::ZendHashTable;
229
    ///
230
    /// let mut ht = ZendHashTable::new();
231
    ///
232
    /// ht.insert("test", "hello world");
233
    /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
234
    /// ```
235
    // TODO: Verify if this is safe to use, as it allows mutating the
236
    // hashtable while only having a reference to it. #461
237
    #[allow(clippy::mut_from_ref)]
238
    #[must_use]
239
    pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval>
×
240
    where
×
241
        K: Into<ArrayKey<'a>>,
×
242
    {
243
        match key.into() {
×
244
            ArrayKey::Long(index) => unsafe {
×
245
                #[allow(clippy::cast_sign_loss)]
246
                zend_hash_index_find(self, index as zend_ulong).as_mut()
×
247
            },
248
            ArrayKey::String(key) => unsafe {
×
249
                zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_mut()
×
250
            },
251
            ArrayKey::Str(key) => unsafe {
×
252
                zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_mut()
×
253
            },
NEW
254
            ArrayKey::ZendString(key) => unsafe {
×
NEW
255
                zend_hash_find(self, key.as_ptr().cast_mut()).as_mut()
×
256
            },
257
        }
258
    }
×
259

260
    /// Attempts to retrieve a value from the hash table with an index.
261
    ///
262
    /// # Parameters
263
    ///
264
    /// * `key` - The key to search for in the hash table.
265
    ///
266
    /// # Returns
267
    ///
268
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
269
    ///   table.
270
    /// * `None` - No value at the given position was found.
271
    ///
272
    /// # Example
273
    ///
274
    /// ```no_run
275
    /// use ext_php_rs::types::ZendHashTable;
276
    ///
277
    /// let mut ht = ZendHashTable::new();
278
    ///
279
    /// ht.push(100);
280
    /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
281
    /// ```
282
    #[must_use]
283
    pub fn get_index(&self, key: i64) -> Option<&Zval> {
1✔
284
        #[allow(clippy::cast_sign_loss)]
285
        unsafe {
286
            zend_hash_index_find(self, key as zend_ulong).as_ref()
1✔
287
        }
288
    }
1✔
289

290
    /// Attempts to retrieve a value from the hash table with an index.
291
    ///
292
    /// # Parameters
293
    ///
294
    /// * `key` - The key to search for in the hash table.
295
    ///
296
    /// # Returns
297
    ///
298
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
299
    ///   table.
300
    /// * `None` - No value at the given position was found.
301
    ///
302
    /// # Example
303
    ///
304
    /// ```no_run
305
    /// use ext_php_rs::types::ZendHashTable;
306
    ///
307
    /// let mut ht = ZendHashTable::new();
308
    ///
309
    /// ht.push(100);
310
    /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
311
    /// ```
312
    // TODO: Verify if this is safe to use, as it allows mutating the
313
    // hashtable while only having a reference to it. #461
314
    #[allow(clippy::mut_from_ref)]
315
    #[must_use]
316
    pub fn get_index_mut(&self, key: i64) -> Option<&mut Zval> {
×
317
        unsafe {
318
            #[allow(clippy::cast_sign_loss)]
319
            zend_hash_index_find(self, key as zend_ulong).as_mut()
×
320
        }
321
    }
×
322

323
    /// Attempts to remove a value from the hash table with a string key.
324
    ///
325
    /// # Parameters
326
    ///
327
    /// * `key` - The key to remove from the hash table.
328
    ///
329
    /// # Returns
330
    ///
331
    /// * `Some(())` - Key was successfully removed.
332
    /// * `None` - No key was removed, did not exist.
333
    ///
334
    /// # Example
335
    ///
336
    /// ```no_run
337
    /// use ext_php_rs::types::ZendHashTable;
338
    ///
339
    /// let mut ht = ZendHashTable::new();
340
    ///
341
    /// ht.insert("test", "hello world");
342
    /// assert_eq!(ht.len(), 1);
343
    ///
344
    /// ht.remove("test");
345
    /// assert_eq!(ht.len(), 0);
346
    /// ```
347
    pub fn remove<'a, K>(&mut self, key: K) -> Option<()>
1✔
348
    where
1✔
349
        K: Into<ArrayKey<'a>>,
1✔
350
    {
351
        let result = match key.into() {
1✔
352
            ArrayKey::Long(index) => unsafe {
×
353
                #[allow(clippy::cast_sign_loss)]
354
                zend_hash_index_del(self, index as zend_ulong)
×
355
            },
356
            ArrayKey::String(key) => unsafe {
×
357
                zend_hash_str_del(self, key.as_ptr().cast(), key.len() as _)
×
358
            },
359
            ArrayKey::Str(key) => unsafe {
1✔
360
                zend_hash_str_del(self, key.as_ptr().cast(), key.len() as _)
1✔
361
            },
NEW
362
            ArrayKey::ZendString(key) => unsafe { zend_hash_del(self, key.as_ptr().cast_mut()) },
×
363
        };
364

365
        if result < 0 {
1✔
NEW
366
            None
×
367
        } else {
368
            Some(())
1✔
369
        }
370
    }
1✔
371

372
    /// Attempts to remove a value from the hash table with a string key.
373
    ///
374
    /// # Parameters
375
    ///
376
    /// * `key` - The key to remove from the hash table.
377
    ///
378
    /// # Returns
379
    ///
380
    /// * `Ok(())` - Key was successfully removed.
381
    /// * `None` - No key was removed, did not exist.
382
    ///
383
    /// # Example
384
    ///
385
    /// ```no_run
386
    /// use ext_php_rs::types::ZendHashTable;
387
    ///
388
    /// let mut ht = ZendHashTable::new();
389
    ///
390
    /// ht.push("hello");
391
    /// assert_eq!(ht.len(), 1);
392
    ///
393
    /// ht.remove_index(0);
394
    /// assert_eq!(ht.len(), 0);
395
    /// ```
396
    pub fn remove_index(&mut self, key: i64) -> Option<()> {
×
397
        let result = unsafe {
×
398
            #[allow(clippy::cast_sign_loss)]
399
            zend_hash_index_del(self, key as zend_ulong)
×
400
        };
401

NEW
402
        if result < 0 {
×
NEW
403
            None
×
404
        } else {
NEW
405
            Some(())
×
406
        }
UNCOV
407
    }
×
408

409
    /// Attempts to insert an item into the hash table, or update if the key
410
    /// already exists. Returns nothing in a result if successful.
411
    ///
412
    /// # Parameters
413
    ///
414
    /// * `key` - The key to insert the value at in the hash table.
415
    /// * `value` - The value to insert into the hash table.
416
    ///
417
    /// # Returns
418
    ///
419
    /// Returns nothing in a result on success.
420
    ///
421
    /// # Errors
422
    ///
423
    /// Returns an error if converting the value into a [`Zval`] failed.
424
    ///
425
    /// # Example
426
    ///
427
    /// ```no_run
428
    /// use ext_php_rs::types::ZendHashTable;
429
    ///
430
    /// let mut ht = ZendHashTable::new();
431
    ///
432
    /// ht.insert("a", "A");
433
    /// ht.insert("b", "B");
434
    /// ht.insert("c", "C");
435
    /// assert_eq!(ht.len(), 3);
436
    /// ```
437
    pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()>
189✔
438
    where
189✔
439
        K: Into<ArrayKey<'a>>,
189✔
440
        V: IntoZval,
189✔
441
    {
442
        let mut val = val.into_zval(false)?;
189✔
443
        match key.into() {
189✔
444
            ArrayKey::Long(index) => {
108✔
445
                unsafe {
108✔
446
                    #[allow(clippy::cast_sign_loss)]
108✔
447
                    zend_hash_index_update(self, index as zend_ulong, &raw mut val)
108✔
448
                };
108✔
449
            }
108✔
450
            ArrayKey::String(key) => {
7✔
451
                unsafe {
7✔
452
                    // Use raw bytes directly since zend_hash_str_update takes a length.
7✔
453
                    // This allows keys with embedded null bytes (e.g. PHP property mangling).
7✔
454
                    zend_hash_str_update(
7✔
455
                        self,
7✔
456
                        key.as_str().as_ptr().cast(),
7✔
457
                        key.len(),
7✔
458
                        &raw mut val,
7✔
459
                    )
7✔
460
                };
7✔
461
            }
7✔
462
            ArrayKey::Str(key) => {
73✔
463
                unsafe {
73✔
464
                    // Use raw bytes directly since zend_hash_str_update takes a length.
73✔
465
                    // This allows keys with embedded null bytes (e.g. PHP property mangling).
73✔
466
                    zend_hash_str_update(self, key.as_ptr().cast(), key.len(), &raw mut val)
73✔
467
                };
73✔
468
            }
73✔
469
            ArrayKey::ZendString(key) => {
1✔
470
                unsafe {
1✔
471
                    // zend_hash_update does the addref itself for non-interned strings.
1✔
472
                    zend_hash_update(self, key.as_ptr().cast_mut(), &raw mut val)
1✔
473
                };
1✔
474
            }
1✔
475
        }
476
        val.release();
189✔
477
        Ok(())
189✔
478
    }
189✔
479

480
    /// Inserts an item into the hash table at a specified index, or updates if
481
    /// the key already exists. Returns nothing in a result if successful.
482
    ///
483
    /// # Parameters
484
    ///
485
    /// * `key` - The index at which the value should be inserted.
486
    /// * `val` - The value to insert into the hash table.
487
    ///
488
    /// # Returns
489
    ///
490
    /// Returns nothing in a result on success.
491
    ///
492
    /// # Errors
493
    ///
494
    /// Returns an error if converting the value into a [`Zval`] failed.
495
    ///
496
    /// # Example
497
    ///
498
    /// ```no_run
499
    /// use ext_php_rs::types::ZendHashTable;
500
    ///
501
    /// let mut ht = ZendHashTable::new();
502
    ///
503
    /// ht.insert_at_index(0, "A");
504
    /// ht.insert_at_index(5, "B");
505
    /// ht.insert_at_index(0, "C"); // notice overriding index 0
506
    /// assert_eq!(ht.len(), 2);
507
    /// ```
508
    pub fn insert_at_index<V>(&mut self, key: i64, val: V) -> Result<()>
×
509
    where
×
510
        V: IntoZval,
×
511
    {
512
        let mut val = val.into_zval(false)?;
×
513
        unsafe {
514
            #[allow(clippy::cast_sign_loss)]
515
            zend_hash_index_update(self, key as zend_ulong, &raw mut val)
×
516
        };
517
        val.release();
×
518
        Ok(())
×
519
    }
×
520

521
    /// Pushes an item onto the end of the hash table. Returns a result
522
    /// containing nothing if the element was successfully inserted.
523
    ///
524
    /// # Parameters
525
    ///
526
    /// * `val` - The value to insert into the hash table.
527
    ///
528
    /// # Returns
529
    ///
530
    /// Returns nothing in a result on success.
531
    ///
532
    /// # Errors
533
    ///
534
    /// Returns an error if converting the value into a [`Zval`] failed.
535
    ///
536
    /// # Example
537
    ///
538
    /// ```no_run
539
    /// use ext_php_rs::types::ZendHashTable;
540
    ///
541
    /// let mut ht = ZendHashTable::new();
542
    ///
543
    /// ht.push("a");
544
    /// ht.push("b");
545
    /// ht.push("c");
546
    /// assert_eq!(ht.len(), 3);
547
    /// ```
548
    pub fn push<V>(&mut self, val: V) -> Result<()>
7✔
549
    where
7✔
550
        V: IntoZval,
7✔
551
    {
552
        let mut val = val.into_zval(false)?;
7✔
553
        unsafe { zend_hash_next_index_insert(self, &raw mut val) };
7✔
554
        val.release();
7✔
555

556
        Ok(())
7✔
557
    }
7✔
558

559
    /// Checks if the hashtable only contains numerical keys.
560
    ///
561
    /// # Returns
562
    ///
563
    /// True if all keys on the hashtable are numerical.
564
    ///
565
    /// # Example
566
    ///
567
    /// ```no_run
568
    /// use ext_php_rs::types::ZendHashTable;
569
    ///
570
    /// let mut ht = ZendHashTable::new();
571
    ///
572
    /// ht.push(0);
573
    /// ht.push(3);
574
    /// ht.push(9);
575
    /// assert!(ht.has_numerical_keys());
576
    ///
577
    /// ht.insert("obviously not numerical", 10);
578
    /// assert!(!ht.has_numerical_keys());
579
    /// ```
580
    #[must_use]
581
    pub fn has_numerical_keys(&self) -> bool {
×
582
        !self.into_iter().any(|(k, _)| !k.is_long())
×
583
    }
×
584

585
    /// Checks if the hashtable has numerical, sequential keys.
586
    ///
587
    /// # Returns
588
    ///
589
    /// True if all keys on the hashtable are numerical and are in sequential
590
    /// order (i.e. starting at 0 and not skipping any keys).
591
    ///
592
    /// # Panics
593
    ///
594
    /// Panics if the number of elements in the hashtable exceeds `i64::MAX`.
595
    ///
596
    /// # Example
597
    ///
598
    /// ```no_run
599
    /// use ext_php_rs::types::ZendHashTable;
600
    ///
601
    /// let mut ht = ZendHashTable::new();
602
    ///
603
    /// ht.push(0);
604
    /// ht.push(3);
605
    /// ht.push(9);
606
    /// assert!(ht.has_sequential_keys());
607
    ///
608
    /// ht.insert_at_index(90, 10);
609
    /// assert!(!ht.has_sequential_keys());
610
    /// ```
611
    #[must_use]
612
    pub fn has_sequential_keys(&self) -> bool {
×
613
        !self
×
614
            .into_iter()
×
615
            .enumerate()
×
616
            .any(|(i, (k, _))| ArrayKey::Long(i64::try_from(i).expect("Integer overflow")) != k)
×
617
    }
×
618

619
    /// Returns an iterator over the values contained inside the hashtable, as
620
    /// if it was a set or list.
621
    ///
622
    /// # Example
623
    ///
624
    /// ```no_run
625
    /// use ext_php_rs::types::ZendHashTable;
626
    ///
627
    /// let mut ht = ZendHashTable::new();
628
    ///
629
    /// for val in ht.values() {
630
    ///     dbg!(val);
631
    /// }
632
    #[inline]
633
    #[must_use]
634
    pub fn values(&self) -> Values<'_> {
×
635
        Values::new(self)
×
636
    }
×
637

638
    /// Returns an iterator over the key(s) and value contained inside the
639
    /// hashtable.
640
    ///
641
    /// # Example
642
    ///
643
    /// ```no_run
644
    /// use ext_php_rs::types::{ZendHashTable, ArrayKey};
645
    ///
646
    /// let mut ht = ZendHashTable::new();
647
    ///
648
    /// for (key, val) in ht.iter() {
649
    ///     match &key {
650
    ///         ArrayKey::Long(index) => {
651
    ///         }
652
    ///         ArrayKey::String(key) => {
653
    ///         }
654
    ///         ArrayKey::Str(key) => {
655
    ///         }
656
    ///         ArrayKey::ZendString(key) => {
657
    ///         }
658
    ///     }
659
    ///     dbg!(key, val);
660
    /// }
661
    #[inline]
662
    #[must_use]
663
    pub fn iter(&self) -> Iter<'_> {
2✔
664
        self.into_iter()
2✔
665
    }
2✔
666

667
    /// Gets the given key's corresponding entry in the hashtable for in-place
668
    /// manipulation.
669
    ///
670
    /// This API is similar to Rust's [`std::collections::hash_map::HashMap::entry`].
671
    ///
672
    /// # Parameters
673
    ///
674
    /// * `key` - The key to look up in the hashtable.
675
    ///
676
    /// # Returns
677
    ///
678
    /// An `Entry` enum that can be used to insert or modify the value at
679
    /// the given key.
680
    ///
681
    /// # Example
682
    ///
683
    /// ```no_run
684
    /// use ext_php_rs::types::ZendHashTable;
685
    ///
686
    /// let mut ht = ZendHashTable::new();
687
    ///
688
    /// // Insert a default value if the key doesn't exist
689
    /// ht.entry("counter").or_insert(0i64);
690
    ///
691
    /// // Modify the value if it exists
692
    /// ht.entry("counter").and_modify(|v| {
693
    ///     if let Some(n) = v.long() {
694
    ///         v.set_long(n + 1);
695
    ///     }
696
    /// });
697
    ///
698
    /// // Use or_insert_with for lazy initialization
699
    /// ht.entry("computed").or_insert_with(|| "computed value");
700
    ///
701
    /// // Works with numeric keys too
702
    /// ht.entry(42i64).or_insert("value at index 42");
703
    /// ```
704
    #[must_use]
705
    pub fn entry<'a, 'k, K>(&'a mut self, key: K) -> Entry<'a, 'k>
11✔
706
    where
11✔
707
        K: Into<ArrayKey<'k>>,
11✔
708
    {
709
        let key = key.into();
11✔
710
        if self.has_key(&key) {
11✔
711
            Entry::Occupied(OccupiedEntry::new(self, key))
4✔
712
        } else {
713
            Entry::Vacant(VacantEntry::new(self, key))
7✔
714
        }
715
    }
11✔
716

717
    /// Checks if a key exists in the hash table.
718
    ///
719
    /// # Parameters
720
    ///
721
    /// * `key` - The key to check for in the hash table.
722
    ///
723
    /// # Returns
724
    ///
725
    /// * `true` - The key exists in the hash table.
726
    /// * `false` - The key does not exist in the hash table.
727
    ///
728
    /// # Example
729
    ///
730
    /// ```no_run
731
    /// use ext_php_rs::types::{ZendHashTable, ArrayKey};
732
    ///
733
    /// let mut ht = ZendHashTable::new();
734
    ///
735
    /// ht.insert("test", "hello world");
736
    /// assert!(ht.has_key(&ArrayKey::from("test")));
737
    /// assert!(!ht.has_key(&ArrayKey::from("missing")));
738
    /// ```
739
    #[must_use]
740
    pub fn has_key(&self, key: &ArrayKey<'_>) -> bool {
19✔
741
        match key {
19✔
742
            ArrayKey::Long(index) => unsafe {
3✔
743
                #[allow(clippy::cast_sign_loss)]
744
                !zend_hash_index_find(self, *index as zend_ulong).is_null()
3✔
745
            },
746
            ArrayKey::String(key) => unsafe {
×
747
                !zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).is_null()
×
748
            },
749
            ArrayKey::Str(key) => unsafe {
14✔
750
                !zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).is_null()
14✔
751
            },
752
            ArrayKey::ZendString(key) => unsafe {
2✔
753
                !zend_hash_find(self, key.as_ptr().cast_mut()).is_null()
2✔
754
            },
755
        }
756
    }
19✔
757

758
    /// Determines whether this hashtable is immutable.
759
    ///
760
    /// Immutable hashtables are shared and cannot be modified. The primary
761
    /// example is the empty immutable shared array returned by
762
    /// [`ZendEmptyArray`].
763
    ///
764
    /// # Example
765
    ///
766
    /// ```no_run
767
    /// use ext_php_rs::types::ZendHashTable;
768
    ///
769
    /// let ht = ZendHashTable::new();
770
    /// assert!(!ht.is_immutable());
771
    /// ```
772
    #[must_use]
773
    pub fn is_immutable(&self) -> bool {
91✔
774
        // SAFETY: Type info is initialized by Zend on array init.
775
        let gc_type_info = unsafe { self.gc.u.type_info };
91✔
776
        let gc_flags = (gc_type_info >> GC_FLAGS_SHIFT) & (GC_FLAGS_MASK >> GC_FLAGS_SHIFT);
91✔
777

778
        gc_flags & ZvalTypeFlags::Immutable.bits() != 0
91✔
779
    }
91✔
780
}
781

782
unsafe impl ZBoxable for ZendHashTable {
783
    fn free(&mut self) {
47✔
784
        // Do not attempt to free the immutable shared empty array.
785
        if self.is_immutable() {
47✔
786
            return;
×
787
        }
47✔
788
        // SAFETY: ZBox has immutable access to `self`.
789
        unsafe { zend_array_destroy(self) }
47✔
790
    }
47✔
791
}
792

793
impl Debug for ZendHashTable {
794
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
795
        f.debug_map()
×
796
            .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
×
797
            .finish()
×
798
    }
×
799
}
800

801
impl ToOwned for ZendHashTable {
802
    type Owned = ZBox<ZendHashTable>;
803

804
    fn to_owned(&self) -> Self::Owned {
×
805
        unsafe {
806
            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
807
            let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
×
808

809
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
810
            ZBox::from_raw(
×
811
                ptr.as_mut()
×
812
                    .expect("Failed to allocate memory for hashtable"),
×
813
            )
814
        }
815
    }
×
816
}
817

818
impl Default for ZBox<ZendHashTable> {
819
    fn default() -> Self {
×
820
        ZendHashTable::new()
×
821
    }
×
822
}
823

824
impl Clone for ZBox<ZendHashTable> {
825
    fn clone(&self) -> Self {
×
826
        (**self).to_owned()
×
827
    }
×
828
}
829

830
impl IntoZval for ZBox<ZendHashTable> {
831
    const TYPE: DataType = DataType::Array;
832
    const NULLABLE: bool = false;
833

834
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1✔
835
        zv.set_hashtable(self);
1✔
836
        Ok(())
1✔
837
    }
1✔
838
}
839

840
impl<'a> FromZval<'a> for &'a ZendHashTable {
841
    const TYPE: DataType = DataType::Array;
842

843
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
844
        zval.array()
×
845
    }
×
846
}
847

848
impl<'a> FromZvalMut<'a> for &'a mut ZendHashTable {
849
    const TYPE: DataType = DataType::Array;
850

851
    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
×
852
        zval.array_mut()
×
853
    }
×
854
}
855

856
/// Represents an empty, immutable, shared PHP array.
857
///
858
/// Since PHP 7.3, it's possible for extensions to return a zval backed by
859
/// an immutable shared hashtable. This helps avoid redundant hashtable
860
/// allocations when returning empty arrays to userland PHP code.
861
///
862
/// This struct provides a safe way to return an empty array without allocating
863
/// a new hashtable. It implements [`IntoZval`] so it can be used as a return
864
/// type for PHP functions.
865
///
866
/// # Safety
867
///
868
/// Unlike [`ZendHashTable`], this type does not allow any mutation of the
869
/// underlying array, as it points to a shared static empty array in PHP's
870
/// memory.
871
///
872
/// # Example
873
///
874
/// ```rust,ignore
875
/// use ext_php_rs::prelude::*;
876
/// use ext_php_rs::types::ZendEmptyArray;
877
///
878
/// #[php_function]
879
/// pub fn get_empty_array() -> ZendEmptyArray {
880
///     ZendEmptyArray
881
/// }
882
/// ```
883
///
884
/// This is more efficient than returning `Vec::<i32>::new()` or creating
885
/// a new `ZendHashTable` when you know the result will be empty.
886
#[derive(Debug, Clone, Copy, Default)]
887
pub struct ZendEmptyArray;
888

889
impl ZendEmptyArray {
890
    /// Returns a reference to the underlying immutable empty hashtable.
891
    ///
892
    /// # Example
893
    ///
894
    /// ```no_run
895
    /// use ext_php_rs::types::ZendEmptyArray;
896
    ///
897
    /// let empty = ZendEmptyArray;
898
    /// let ht = empty.as_hashtable();
899
    /// assert!(ht.is_empty());
900
    /// assert!(ht.is_immutable());
901
    /// ```
902
    #[must_use]
903
    pub fn as_hashtable(&self) -> &ZendHashTable {
1✔
904
        // SAFETY: zend_empty_array is a static global initialized by PHP.
905
        unsafe { &zend_empty_array }
1✔
906
    }
1✔
907
}
908

909
impl IntoZval for ZendEmptyArray {
910
    const TYPE: DataType = DataType::Array;
911
    const NULLABLE: bool = false;
912

913
    fn set_zval(self, zv: &mut Zval, _persistent: bool) -> Result<()> {
1✔
914
        // Set the zval to point to the immutable shared empty array.
915
        // This mirrors the ZVAL_EMPTY_ARRAY macro in PHP.
916
        zv.u1.type_info = ZvalTypeFlags::Array.bits();
1✔
917
        zv.value.arr = ptr::from_ref(self.as_hashtable()).cast_mut();
1✔
918
        Ok(())
1✔
919
    }
1✔
920
}
921

922
#[cfg(test)]
923
#[cfg(feature = "embed")]
924
mod tests {
925
    use super::*;
926
    use crate::embed::Embed;
927
    use crate::types::ZendStr;
928

929
    #[test]
930
    fn test_has_key_string() {
1✔
931
        Embed::run(|| {
1✔
932
            let mut ht = ZendHashTable::new();
1✔
933
            let _ = ht.insert("test", "value");
1✔
934

935
            assert!(ht.has_key(&ArrayKey::from("test")));
1✔
936
            assert!(!ht.has_key(&ArrayKey::from("missing")));
1✔
937
        });
1✔
938
    }
1✔
939

940
    #[test]
941
    fn test_has_key_long() {
1✔
942
        Embed::run(|| {
1✔
943
            let mut ht = ZendHashTable::new();
1✔
944
            let _ = ht.push(42i64);
1✔
945

946
            assert!(ht.has_key(&ArrayKey::Long(0)));
1✔
947
            assert!(!ht.has_key(&ArrayKey::Long(1)));
1✔
948
        });
1✔
949
    }
1✔
950

951
    #[test]
952
    fn test_has_key_str_ref() {
1✔
953
        Embed::run(|| {
1✔
954
            let mut ht = ZendHashTable::new();
1✔
955
            let _ = ht.insert("hello", "world");
1✔
956

957
            let key = ArrayKey::Str("hello");
1✔
958
            assert!(ht.has_key(&key));
1✔
959
            // Key is still usable after has_key (no clone needed)
960
            assert!(ht.has_key(&key));
1✔
961

962
            assert!(!ht.has_key(&ArrayKey::Str("missing")));
1✔
963
        });
1✔
964
    }
1✔
965

966
    #[test]
967
    fn test_has_key_zend_string() {
1✔
968
        Embed::run(|| {
1✔
969
            let mut ht = ZendHashTable::new();
1✔
970
            let key = ZendStr::new("hello", false);
1✔
971

972
            let _ = ht.insert(&key, "world");
1✔
973
            assert!(ht.has_key(&ArrayKey::ZendString(&key)));
1✔
974
        });
1✔
975
    }
1✔
976
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc