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

extphprs / ext-php-rs / 21330140731

25 Jan 2026 09:08AM UTC coverage: 35.88% (+0.9%) from 34.976%
21330140731

Pull #611

github

web-flow
Merge 876e7a6d0 into 6013c2e62
Pull Request #611: feat(array): Entry API (Issue #525)

62 of 100 new or added lines in 2 files covered. (62.0%)

59 existing lines in 1 file now uncovered.

1907 of 5315 relevant lines covered (35.88%)

14.4 hits per line

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

40.32
/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, ffi::CString, 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, GC_FLAGS_MASK, GC_FLAGS_SHIFT, HT_MIN_SIZE, zend_array_count,
13
        zend_array_destroy, zend_array_dup, zend_empty_array, zend_hash_clean, zend_hash_index_del,
14
        zend_hash_index_find, zend_hash_index_update, zend_hash_next_index_insert,
15
        zend_hash_str_del, zend_hash_str_find, zend_hash_str_update,
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> {
41✔
74
        Self::with_capacity(HT_MIN_SIZE)
41✔
75
    }
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> {
55✔
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);
165✔
101

102
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
103
            ZBox::from_raw(
104
                ptr.as_mut()
165✔
105
                    .expect("Failed to allocate memory for hashtable"),
55✔
106
            )
107
        }
108
    }
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 {
60✔
126
        unsafe { zend_array_count(ptr::from_ref(self).cast_mut()) as usize }
180✔
127
    }
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>
45✔
193
    where
194
        K: Into<ArrayKey<'a>>,
195
    {
196
        match key.into() {
45✔
197
            ArrayKey::Long(index) => unsafe {
198
                #[allow(clippy::cast_sign_loss)]
199
                zend_hash_index_find(self, index as zend_ulong).as_ref()
80✔
200
            },
201
            ArrayKey::String(key) => unsafe {
202
                zend_hash_str_find(
203
                    self,
×
UNCOV
204
                    CString::new(key.as_str()).ok()?.as_ptr(),
×
205
                    key.len() as _,
×
206
                )
207
                .as_ref()
208
            },
209
            ArrayKey::Str(key) => unsafe {
210
                zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _).as_ref()
225✔
211
            },
212
        }
213
    }
214

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

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

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

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

372
        if result < 0 { None } else { Some(()) }
2✔
373
    }
374

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

UNCOV
405
        if result < 0 { None } else { Some(()) }
×
406
    }
407

408
    /// Attempts to insert an item into the hash table, or update if the key
409
    /// already exists. Returns nothing in a result if successful.
410
    ///
411
    /// # Parameters
412
    ///
413
    /// * `key` - The key to insert the value at in the hash table.
414
    /// * `value` - The value to insert into the hash table.
415
    ///
416
    /// # Returns
417
    ///
418
    /// Returns nothing in a result on success.
419
    ///
420
    /// # Errors
421
    ///
422
    /// Returns an error if the key could not be converted into a [`CString`],
423
    /// or 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<()>
118✔
438
    where
439
        K: Into<ArrayKey<'a>>,
440
        V: IntoZval,
441
    {
442
        let mut val = val.into_zval(false)?;
354✔
443
        match key.into() {
118✔
444
            ArrayKey::Long(index) => {
130✔
445
                unsafe {
446
                    #[allow(clippy::cast_sign_loss)]
447
                    zend_hash_index_update(self, index as zend_ulong, &raw mut val)
195✔
448
                };
449
            }
UNCOV
450
            ArrayKey::String(key) => {
×
451
                unsafe {
452
                    // Use raw bytes directly since zend_hash_str_update takes a length.
453
                    // This allows keys with embedded null bytes (e.g. PHP property mangling).
454
                    zend_hash_str_update(
UNCOV
455
                        self,
×
UNCOV
456
                        key.as_str().as_ptr().cast(),
×
UNCOV
457
                        key.len(),
×
UNCOV
458
                        &raw mut val,
×
459
                    )
460
                };
461
            }
462
            ArrayKey::Str(key) => {
106✔
463
                unsafe {
464
                    // Use raw bytes directly since zend_hash_str_update takes a length.
465
                    // This allows keys with embedded null bytes (e.g. PHP property mangling).
466
                    zend_hash_str_update(self, key.as_ptr().cast(), key.len(), &raw mut val)
371✔
467
                };
468
            }
469
        }
470
        val.release();
236✔
471
        Ok(())
118✔
472
    }
473

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

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

550
        Ok(())
1✔
551
    }
552

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

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

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

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

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

708
    /// Checks if a key exists in the hash table.
709
    ///
710
    /// # Parameters
711
    ///
712
    /// * `key` - The key to check for in the hash table.
713
    ///
714
    /// # Returns
715
    ///
716
    /// * `true` - The key exists in the hash table.
717
    /// * `false` - The key does not exist in the hash table.
718
    ///
719
    /// # Example
720
    ///
721
    /// ```no_run
722
    /// use ext_php_rs::types::{ZendHashTable, ArrayKey};
723
    ///
724
    /// let mut ht = ZendHashTable::new();
725
    ///
726
    /// ht.insert("test", "hello world");
727
    /// assert!(ht.has_key(&ArrayKey::from("test")));
728
    /// assert!(!ht.has_key(&ArrayKey::from("missing")));
729
    /// ```
730
    #[must_use]
731
    pub fn has_key(&self, key: &ArrayKey<'_>) -> bool {
16✔
732
        match key {
16✔
733
            ArrayKey::Long(index) => unsafe {
734
                #[allow(clippy::cast_sign_loss)]
735
                !zend_hash_index_find(self, *index as zend_ulong).is_null()
9✔
736
            },
NEW
UNCOV
737
            ArrayKey::String(key) => {
×
NEW
UNCOV
738
                let Ok(cstr) = CString::new(key.as_str()) else {
×
NEW
UNCOV
739
                    return false;
×
740
                };
NEW
UNCOV
741
                unsafe { !zend_hash_str_find(self, cstr.as_ptr(), key.len() as _).is_null() }
×
742
            }
743
            ArrayKey::Str(key) => {
13✔
744
                let Ok(cstr) = CString::new(*key) else {
26✔
NEW
UNCOV
745
                    return false;
×
746
                };
747
                unsafe { !zend_hash_str_find(self, cstr.as_ptr(), key.len() as _).is_null() }
65✔
748
            }
749
        }
750
    }
751

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

772
        gc_flags & ZvalTypeFlags::Immutable.bits() != 0
110✔
773
    }
774
}
775

776
unsafe impl ZBoxable for ZendHashTable {
777
    fn free(&mut self) {
34✔
778
        // Do not attempt to free the immutable shared empty array.
779
        if self.is_immutable() {
68✔
UNCOV
780
            return;
×
781
        }
782
        // SAFETY: ZBox has immutable access to `self`.
783
        unsafe { zend_array_destroy(self) }
68✔
784
    }
785
}
786

787
impl Debug for ZendHashTable {
UNCOV
788
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
UNCOV
789
        f.debug_map()
×
UNCOV
790
            .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
×
791
            .finish()
792
    }
793
}
794

795
impl ToOwned for ZendHashTable {
796
    type Owned = ZBox<ZendHashTable>;
797

UNCOV
798
    fn to_owned(&self) -> Self::Owned {
×
799
        unsafe {
800
            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
UNCOV
801
            let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
×
802

803
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
804
            ZBox::from_raw(
UNCOV
805
                ptr.as_mut()
×
UNCOV
806
                    .expect("Failed to allocate memory for hashtable"),
×
807
            )
808
        }
809
    }
810
}
811

812
impl Default for ZBox<ZendHashTable> {
UNCOV
813
    fn default() -> Self {
×
UNCOV
814
        ZendHashTable::new()
×
815
    }
816
}
817

818
impl Clone for ZBox<ZendHashTable> {
UNCOV
819
    fn clone(&self) -> Self {
×
UNCOV
820
        (**self).to_owned()
×
821
    }
822
}
823

824
impl IntoZval for ZBox<ZendHashTable> {
825
    const TYPE: DataType = DataType::Array;
826
    const NULLABLE: bool = false;
827

UNCOV
828
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
UNCOV
829
        zv.set_hashtable(self);
×
UNCOV
830
        Ok(())
×
831
    }
832
}
833

834
impl<'a> FromZval<'a> for &'a ZendHashTable {
835
    const TYPE: DataType = DataType::Array;
836

UNCOV
837
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
838
        zval.array()
×
839
    }
840
}
841

842
impl<'a> FromZvalMut<'a> for &'a mut ZendHashTable {
843
    const TYPE: DataType = DataType::Array;
844

UNCOV
845
    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
×
UNCOV
846
        zval.array_mut()
×
847
    }
848
}
849

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

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

903
impl IntoZval for ZendEmptyArray {
904
    const TYPE: DataType = DataType::Array;
905
    const NULLABLE: bool = false;
906

907
    fn set_zval(self, zv: &mut Zval, _persistent: bool) -> Result<()> {
×
908
        // Set the zval to point to the immutable shared empty array.
909
        // This mirrors the ZVAL_EMPTY_ARRAY macro in PHP.
910
        zv.u1.type_info = ZvalTypeFlags::Array.bits();
×
911
        zv.value.arr = ptr::from_ref(self.as_hashtable()).cast_mut();
×
912
        Ok(())
×
913
    }
914
}
915

916
#[cfg(test)]
917
#[cfg(feature = "embed")]
918
mod tests {
919
    use super::*;
920
    use crate::embed::Embed;
921

922
    #[test]
923
    fn test_has_key_string() {
924
        Embed::run(|| {
925
            let mut ht = ZendHashTable::new();
926
            let _ = ht.insert("test", "value");
927

928
            assert!(ht.has_key(&ArrayKey::from("test")));
929
            assert!(!ht.has_key(&ArrayKey::from("missing")));
930
        });
931
    }
932

933
    #[test]
934
    fn test_has_key_long() {
935
        Embed::run(|| {
936
            let mut ht = ZendHashTable::new();
937
            let _ = ht.push(42i64);
938

939
            assert!(ht.has_key(&ArrayKey::Long(0)));
940
            assert!(!ht.has_key(&ArrayKey::Long(1)));
941
        });
942
    }
943

944
    #[test]
945
    fn test_has_key_str_ref() {
946
        Embed::run(|| {
947
            let mut ht = ZendHashTable::new();
948
            let _ = ht.insert("hello", "world");
949

950
            let key = ArrayKey::Str("hello");
951
            assert!(ht.has_key(&key));
952
            // Key is still usable after has_key (no clone needed)
953
            assert!(ht.has_key(&key));
954

955
            assert!(!ht.has_key(&ArrayKey::Str("missing")));
956
        });
957
    }
958
}
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