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

extphprs / ext-php-rs / 20346131039

18 Dec 2025 05:52PM UTC coverage: 37.017% (+1.0%) from 36.015%
20346131039

Pull #611

github

web-flow
Merge 6b37357b1 into 62c1aa7d6
Pull Request #611: feat(array): Entry API (Issue #525)

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

54 existing lines in 1 file now uncovered.

1735 of 4687 relevant lines covered (37.02%)

11.27 hits per line

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

40.91
/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, IntoZval},
9
    error::Result,
10
    ffi::zend_ulong,
11
    ffi::{
12
        _zend_new_array, HT_MIN_SIZE, zend_array_count, zend_array_destroy, zend_array_dup,
13
        zend_hash_clean, zend_hash_index_del, zend_hash_index_find, zend_hash_index_update,
14
        zend_hash_next_index_insert, zend_hash_str_del, zend_hash_str_find, zend_hash_str_update,
15
    },
16
    flags::DataType,
17
    types::Zval,
18
};
19

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

407
    /// Attempts to insert an item into the hash table, or update if the key
408
    /// already exists. Returns nothing in a result if successful.
409
    ///
410
    /// # Parameters
411
    ///
412
    /// * `key` - The key to insert the value at in the hash table.
413
    /// * `value` - The value to insert into the hash table.
414
    ///
415
    /// # Returns
416
    ///
417
    /// Returns nothing in a result on success.
418
    ///
419
    /// # Errors
420
    ///
421
    /// Returns an error if the key could not be converted into a [`CString`],
422
    /// or converting the value into a [`Zval`] failed.
423
    ///
424
    /// # Example
425
    ///
426
    /// ```no_run
427
    /// use ext_php_rs::types::ZendHashTable;
428
    ///
429
    /// let mut ht = ZendHashTable::new();
430
    ///
431
    /// ht.insert("a", "A");
432
    /// ht.insert("b", "B");
433
    /// ht.insert("c", "C");
434
    /// assert_eq!(ht.len(), 3);
435
    /// ```
436
    pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()>
118✔
437
    where
438
        K: Into<ArrayKey<'a>>,
439
        V: IntoZval,
440
    {
441
        let mut val = val.into_zval(false)?;
354✔
442
        match key.into() {
118✔
443
            ArrayKey::Long(index) => {
130✔
444
                unsafe {
445
                    #[allow(clippy::cast_sign_loss)]
446
                    zend_hash_index_update(self, index as zend_ulong, &raw mut val)
195✔
447
                };
448
            }
UNCOV
449
            ArrayKey::String(key) => {
×
450
                unsafe {
451
                    zend_hash_str_update(
UNCOV
452
                        self,
×
UNCOV
453
                        CString::new(key.as_str())?.as_ptr(),
×
UNCOV
454
                        key.len(),
×
UNCOV
455
                        &raw mut val,
×
456
                    )
457
                };
458
            }
459
            ArrayKey::Str(key) => {
53✔
460
                unsafe {
461
                    zend_hash_str_update(self, CString::new(key)?.as_ptr(), key.len(), &raw mut val)
424✔
462
                };
463
            }
464
        }
465
        val.release();
236✔
466
        Ok(())
118✔
467
    }
468

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

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

545
        Ok(())
1✔
546
    }
547

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

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

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

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

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

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

748
unsafe impl ZBoxable for ZendHashTable {
749
    fn free(&mut self) {
34✔
750
        // SAFETY: ZBox has immutable access to `self`.
751
        unsafe { zend_array_destroy(self) }
68✔
752
    }
753
}
754

755
impl Debug for ZendHashTable {
UNCOV
756
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
UNCOV
757
        f.debug_map()
×
UNCOV
758
            .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
×
759
            .finish()
760
    }
761
}
762

763
impl ToOwned for ZendHashTable {
764
    type Owned = ZBox<ZendHashTable>;
765

UNCOV
766
    fn to_owned(&self) -> Self::Owned {
×
767
        unsafe {
768
            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
UNCOV
769
            let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
×
770

771
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
772
            ZBox::from_raw(
UNCOV
773
                ptr.as_mut()
×
UNCOV
774
                    .expect("Failed to allocate memory for hashtable"),
×
775
            )
776
        }
777
    }
778
}
779

780
impl Default for ZBox<ZendHashTable> {
UNCOV
781
    fn default() -> Self {
×
UNCOV
782
        ZendHashTable::new()
×
783
    }
784
}
785

786
impl Clone for ZBox<ZendHashTable> {
UNCOV
787
    fn clone(&self) -> Self {
×
UNCOV
788
        (**self).to_owned()
×
789
    }
790
}
791

792
impl IntoZval for ZBox<ZendHashTable> {
793
    const TYPE: DataType = DataType::Array;
794
    const NULLABLE: bool = false;
795

UNCOV
796
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
UNCOV
797
        zv.set_hashtable(self);
×
UNCOV
798
        Ok(())
×
799
    }
800
}
801

802
impl<'a> FromZval<'a> for &'a ZendHashTable {
803
    const TYPE: DataType = DataType::Array;
804

UNCOV
805
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
806
        zval.array()
×
807
    }
808
}
809

810
#[cfg(test)]
811
#[cfg(feature = "embed")]
812
mod tests {
813
    use super::*;
814
    use crate::embed::Embed;
815

816
    #[test]
817
    fn test_has_key_string() {
818
        Embed::run(|| {
819
            let mut ht = ZendHashTable::new();
820
            let _ = ht.insert("test", "value");
821

822
            assert!(ht.has_key(&ArrayKey::from("test")));
823
            assert!(!ht.has_key(&ArrayKey::from("missing")));
824
        });
825
    }
826

827
    #[test]
828
    fn test_has_key_long() {
829
        Embed::run(|| {
830
            let mut ht = ZendHashTable::new();
831
            let _ = ht.push(42i64);
832

833
            assert!(ht.has_key(&ArrayKey::Long(0)));
834
            assert!(!ht.has_key(&ArrayKey::Long(1)));
835
        });
836
    }
837

838
    #[test]
839
    fn test_has_key_str_ref() {
840
        Embed::run(|| {
841
            let mut ht = ZendHashTable::new();
842
            let _ = ht.insert("hello", "world");
843

844
            let key = ArrayKey::Str("hello");
845
            assert!(ht.has_key(&key));
846
            // Key is still usable after has_key (no clone needed)
847
            assert!(ht.has_key(&key));
848

849
            assert!(!ht.has_key(&ArrayKey::Str("missing")));
850
        });
851
    }
852
}
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