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

extphprs / ext-php-rs / 21335671668

25 Jan 2026 04:17PM UTC coverage: 35.456% (+0.9%) from 34.595%
21335671668

Pull #611

github

web-flow
Merge fb1362277 into f4f2f3a44
Pull Request #611: feat(array): Entry API (Issue #525)

51 of 87 new or added lines in 2 files covered. (58.62%)

51 existing lines in 1 file now uncovered.

1904 of 5370 relevant lines covered (35.46%)

14.25 hits per line

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

40.83
/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> {
42✔
74
        Self::with_capacity(HT_MIN_SIZE)
42✔
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> {
56✔
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);
168✔
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()
168✔
105
                    .expect("Failed to allocate memory for hashtable"),
56✔
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(),
×
NEW
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]
NEW
655
    pub fn iter(&self) -> Iter<'_> {
×
NEW
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
    #[must_use]
697
    pub fn entry<'a, 'k, K>(&'a mut self, key: K) -> Entry<'a, 'k>
10✔
698
    where
699
        K: Into<ArrayKey<'k>>,
700
    {
701
        let key = key.into();
30✔
702
        if self.has_key(&key) {
30✔
703
            Entry::Occupied(OccupiedEntry::new(self, key))
8✔
704
        } else {
705
            Entry::Vacant(VacantEntry::new(self, key))
12✔
706
        }
707
    }
708

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

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

767
        gc_flags & ZvalTypeFlags::Immutable.bits() != 0
112✔
768
    }
769
}
770

771
unsafe impl ZBoxable for ZendHashTable {
772
    fn free(&mut self) {
35✔
773
        // Do not attempt to free the immutable shared empty array.
774
        if self.is_immutable() {
70✔
UNCOV
775
            return;
×
776
        }
777
        // SAFETY: ZBox has immutable access to `self`.
778
        unsafe { zend_array_destroy(self) }
70✔
779
    }
780
}
781

782
impl Debug for ZendHashTable {
UNCOV
783
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
UNCOV
784
        f.debug_map()
×
785
            .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
×
786
            .finish()
787
    }
788
}
789

790
impl ToOwned for ZendHashTable {
791
    type Owned = ZBox<ZendHashTable>;
792

793
    fn to_owned(&self) -> Self::Owned {
×
794
        unsafe {
795
            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
UNCOV
796
            let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
×
797

798
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
799
            ZBox::from_raw(
800
                ptr.as_mut()
×
UNCOV
801
                    .expect("Failed to allocate memory for hashtable"),
×
802
            )
803
        }
804
    }
805
}
806

807
impl Default for ZBox<ZendHashTable> {
808
    fn default() -> Self {
×
809
        ZendHashTable::new()
×
810
    }
811
}
812

813
impl Clone for ZBox<ZendHashTable> {
UNCOV
814
    fn clone(&self) -> Self {
×
UNCOV
815
        (**self).to_owned()
×
816
    }
817
}
818

819
impl IntoZval for ZBox<ZendHashTable> {
820
    const TYPE: DataType = DataType::Array;
821
    const NULLABLE: bool = false;
822

UNCOV
823
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
UNCOV
824
        zv.set_hashtable(self);
×
825
        Ok(())
×
826
    }
827
}
828

829
impl<'a> FromZval<'a> for &'a ZendHashTable {
830
    const TYPE: DataType = DataType::Array;
831

UNCOV
832
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
833
        zval.array()
×
834
    }
835
}
836

837
impl<'a> FromZvalMut<'a> for &'a mut ZendHashTable {
838
    const TYPE: DataType = DataType::Array;
839

UNCOV
840
    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
×
UNCOV
841
        zval.array_mut()
×
842
    }
843
}
844

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

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

898
impl IntoZval for ZendEmptyArray {
899
    const TYPE: DataType = DataType::Array;
900
    const NULLABLE: bool = false;
901

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

911
#[cfg(test)]
912
#[cfg(feature = "embed")]
913
mod tests {
914
    use super::*;
915
    use crate::embed::Embed;
916

917
    #[test]
918
    fn test_has_key_string() {
919
        Embed::run(|| {
920
            let mut ht = ZendHashTable::new();
921
            let _ = ht.insert("test", "value");
922

923
            assert!(ht.has_key(&ArrayKey::from("test")));
924
            assert!(!ht.has_key(&ArrayKey::from("missing")));
925
        });
926
    }
927

928
    #[test]
929
    fn test_has_key_long() {
930
        Embed::run(|| {
931
            let mut ht = ZendHashTable::new();
932
            let _ = ht.push(42i64);
933

934
            assert!(ht.has_key(&ArrayKey::Long(0)));
935
            assert!(!ht.has_key(&ArrayKey::Long(1)));
936
        });
937
    }
938

939
    #[test]
940
    fn test_has_key_str_ref() {
941
        Embed::run(|| {
942
            let mut ht = ZendHashTable::new();
943
            let _ = ht.insert("hello", "world");
944

945
            let key = ArrayKey::Str("hello");
946
            assert!(ht.has_key(&key));
947
            // Key is still usable after has_key (no clone needed)
948
            assert!(ht.has_key(&key));
949

950
            assert!(!ht.has_key(&ArrayKey::Str("missing")));
951
        });
952
    }
953
}
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