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

extphprs / ext-php-rs / 23718488839

29 Mar 2026 08:35PM UTC coverage: 65.13% (+31.0%) from 34.103%
23718488839

push

github

web-flow
ci(coverage): switch from tarpaulin to cargo-llvm-cov (#702)

7811 of 11993 relevant lines covered (65.13%)

32.6 hits per line

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

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

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

6
use crate::{
7
    boxed::{ZBox, ZBoxable},
8
    convert::{FromZval, FromZvalMut, IntoZval},
9
    error::Result,
10
    ffi::zend_ulong,
11
    ffi::{
12
        _zend_new_array, 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> {
57✔
74
        Self::with_capacity(HT_MIN_SIZE)
57✔
75
    }
57✔
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> {
87✔
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);
87✔
101

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

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

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

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

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

358
        if result < 0 { None } else { Some(()) }
1✔
359
    }
1✔
360

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

391
        if result < 0 { None } else { Some(()) }
×
392
    }
×
393

394
    /// Attempts to insert an item into the hash table, or update if the key
395
    /// already exists. Returns nothing in a result if successful.
396
    ///
397
    /// # Parameters
398
    ///
399
    /// * `key` - The key to insert the value at in the hash table.
400
    /// * `value` - The value to insert into the hash table.
401
    ///
402
    /// # Returns
403
    ///
404
    /// Returns nothing in a result on success.
405
    ///
406
    /// # Errors
407
    ///
408
    /// Returns an error if converting the value into a [`Zval`] failed.
409
    ///
410
    /// # Example
411
    ///
412
    /// ```no_run
413
    /// use ext_php_rs::types::ZendHashTable;
414
    ///
415
    /// let mut ht = ZendHashTable::new();
416
    ///
417
    /// ht.insert("a", "A");
418
    /// ht.insert("b", "B");
419
    /// ht.insert("c", "C");
420
    /// assert_eq!(ht.len(), 3);
421
    /// ```
422
    pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()>
188✔
423
    where
188✔
424
        K: Into<ArrayKey<'a>>,
188✔
425
        V: IntoZval,
188✔
426
    {
427
        let mut val = val.into_zval(false)?;
188✔
428
        match key.into() {
188✔
429
            ArrayKey::Long(index) => {
108✔
430
                unsafe {
108✔
431
                    #[allow(clippy::cast_sign_loss)]
108✔
432
                    zend_hash_index_update(self, index as zend_ulong, &raw mut val)
108✔
433
                };
108✔
434
            }
108✔
435
            ArrayKey::String(key) => {
7✔
436
                unsafe {
7✔
437
                    // Use raw bytes directly since zend_hash_str_update takes a length.
7✔
438
                    // This allows keys with embedded null bytes (e.g. PHP property mangling).
7✔
439
                    zend_hash_str_update(
7✔
440
                        self,
7✔
441
                        key.as_str().as_ptr().cast(),
7✔
442
                        key.len(),
7✔
443
                        &raw mut val,
7✔
444
                    )
7✔
445
                };
7✔
446
            }
7✔
447
            ArrayKey::Str(key) => {
73✔
448
                unsafe {
73✔
449
                    // Use raw bytes directly since zend_hash_str_update takes a length.
73✔
450
                    // This allows keys with embedded null bytes (e.g. PHP property mangling).
73✔
451
                    zend_hash_str_update(self, key.as_ptr().cast(), key.len(), &raw mut val)
73✔
452
                };
73✔
453
            }
73✔
454
        }
455
        val.release();
188✔
456
        Ok(())
188✔
457
    }
188✔
458

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

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

535
        Ok(())
5✔
536
    }
5✔
537

538
    /// Checks if the hashtable only contains numerical keys.
539
    ///
540
    /// # Returns
541
    ///
542
    /// True if all keys on the hashtable are numerical.
543
    ///
544
    /// # Example
545
    ///
546
    /// ```no_run
547
    /// use ext_php_rs::types::ZendHashTable;
548
    ///
549
    /// let mut ht = ZendHashTable::new();
550
    ///
551
    /// ht.push(0);
552
    /// ht.push(3);
553
    /// ht.push(9);
554
    /// assert!(ht.has_numerical_keys());
555
    ///
556
    /// ht.insert("obviously not numerical", 10);
557
    /// assert!(!ht.has_numerical_keys());
558
    /// ```
559
    #[must_use]
560
    pub fn has_numerical_keys(&self) -> bool {
×
561
        !self.into_iter().any(|(k, _)| !k.is_long())
×
562
    }
×
563

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

598
    /// Returns an iterator over the values contained inside the hashtable, as
599
    /// if it was a set or list.
600
    ///
601
    /// # Example
602
    ///
603
    /// ```no_run
604
    /// use ext_php_rs::types::ZendHashTable;
605
    ///
606
    /// let mut ht = ZendHashTable::new();
607
    ///
608
    /// for val in ht.values() {
609
    ///     dbg!(val);
610
    /// }
611
    #[inline]
612
    #[must_use]
613
    pub fn values(&self) -> Values<'_> {
×
614
        Values::new(self)
×
615
    }
×
616

617
    /// Returns an iterator over the key(s) and value contained inside the
618
    /// hashtable.
619
    ///
620
    /// # Example
621
    ///
622
    /// ```no_run
623
    /// use ext_php_rs::types::{ZendHashTable, ArrayKey};
624
    ///
625
    /// let mut ht = ZendHashTable::new();
626
    ///
627
    /// for (key, val) in ht.iter() {
628
    ///     match &key {
629
    ///         ArrayKey::Long(index) => {
630
    ///         }
631
    ///         ArrayKey::String(key) => {
632
    ///         }
633
    ///         ArrayKey::Str(key) => {
634
    ///         }
635
    ///     }
636
    ///     dbg!(key, val);
637
    /// }
638
    #[inline]
639
    #[must_use]
640
    pub fn iter(&self) -> Iter<'_> {
2✔
641
        self.into_iter()
2✔
642
    }
2✔
643

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

694
    /// Checks if a key exists in the hash table.
695
    ///
696
    /// # Parameters
697
    ///
698
    /// * `key` - The key to check for in the hash table.
699
    ///
700
    /// # Returns
701
    ///
702
    /// * `true` - The key exists in the hash table.
703
    /// * `false` - The key does not exist in the hash table.
704
    ///
705
    /// # Example
706
    ///
707
    /// ```no_run
708
    /// use ext_php_rs::types::{ZendHashTable, ArrayKey};
709
    ///
710
    /// let mut ht = ZendHashTable::new();
711
    ///
712
    /// ht.insert("test", "hello world");
713
    /// assert!(ht.has_key(&ArrayKey::from("test")));
714
    /// assert!(!ht.has_key(&ArrayKey::from("missing")));
715
    /// ```
716
    #[must_use]
717
    pub fn has_key(&self, key: &ArrayKey<'_>) -> bool {
17✔
718
        match key {
17✔
719
            ArrayKey::Long(index) => unsafe {
3✔
720
                #[allow(clippy::cast_sign_loss)]
721
                !zend_hash_index_find(self, *index as zend_ulong).is_null()
3✔
722
            },
723
            ArrayKey::String(key) => unsafe {
×
724
                !zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).is_null()
×
725
            },
726
            ArrayKey::Str(key) => unsafe {
14✔
727
                !zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).is_null()
14✔
728
            },
729
        }
730
    }
17✔
731

732
    /// Determines whether this hashtable is immutable.
733
    ///
734
    /// Immutable hashtables are shared and cannot be modified. The primary
735
    /// example is the empty immutable shared array returned by
736
    /// [`ZendEmptyArray`].
737
    ///
738
    /// # Example
739
    ///
740
    /// ```no_run
741
    /// use ext_php_rs::types::ZendHashTable;
742
    ///
743
    /// let ht = ZendHashTable::new();
744
    /// assert!(!ht.is_immutable());
745
    /// ```
746
    #[must_use]
747
    pub fn is_immutable(&self) -> bool {
87✔
748
        // SAFETY: Type info is initialized by Zend on array init.
749
        let gc_type_info = unsafe { self.gc.u.type_info };
87✔
750
        let gc_flags = (gc_type_info >> GC_FLAGS_SHIFT) & (GC_FLAGS_MASK >> GC_FLAGS_SHIFT);
87✔
751

752
        gc_flags & ZvalTypeFlags::Immutable.bits() != 0
87✔
753
    }
87✔
754
}
755

756
unsafe impl ZBoxable for ZendHashTable {
757
    fn free(&mut self) {
45✔
758
        // Do not attempt to free the immutable shared empty array.
759
        if self.is_immutable() {
45✔
760
            return;
×
761
        }
45✔
762
        // SAFETY: ZBox has immutable access to `self`.
763
        unsafe { zend_array_destroy(self) }
45✔
764
    }
45✔
765
}
766

767
impl Debug for ZendHashTable {
768
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
769
        f.debug_map()
×
770
            .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
×
771
            .finish()
×
772
    }
×
773
}
774

775
impl ToOwned for ZendHashTable {
776
    type Owned = ZBox<ZendHashTable>;
777

778
    fn to_owned(&self) -> Self::Owned {
×
779
        unsafe {
780
            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
781
            let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
×
782

783
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
784
            ZBox::from_raw(
×
785
                ptr.as_mut()
×
786
                    .expect("Failed to allocate memory for hashtable"),
×
787
            )
788
        }
789
    }
×
790
}
791

792
impl Default for ZBox<ZendHashTable> {
793
    fn default() -> Self {
×
794
        ZendHashTable::new()
×
795
    }
×
796
}
797

798
impl Clone for ZBox<ZendHashTable> {
799
    fn clone(&self) -> Self {
×
800
        (**self).to_owned()
×
801
    }
×
802
}
803

804
impl IntoZval for ZBox<ZendHashTable> {
805
    const TYPE: DataType = DataType::Array;
806
    const NULLABLE: bool = false;
807

808
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1✔
809
        zv.set_hashtable(self);
1✔
810
        Ok(())
1✔
811
    }
1✔
812
}
813

814
impl<'a> FromZval<'a> for &'a ZendHashTable {
815
    const TYPE: DataType = DataType::Array;
816

817
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
818
        zval.array()
×
819
    }
×
820
}
821

822
impl<'a> FromZvalMut<'a> for &'a mut ZendHashTable {
823
    const TYPE: DataType = DataType::Array;
824

825
    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
×
826
        zval.array_mut()
×
827
    }
×
828
}
829

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

863
impl ZendEmptyArray {
864
    /// Returns a reference to the underlying immutable empty hashtable.
865
    ///
866
    /// # Example
867
    ///
868
    /// ```no_run
869
    /// use ext_php_rs::types::ZendEmptyArray;
870
    ///
871
    /// let empty = ZendEmptyArray;
872
    /// let ht = empty.as_hashtable();
873
    /// assert!(ht.is_empty());
874
    /// assert!(ht.is_immutable());
875
    /// ```
876
    #[must_use]
877
    pub fn as_hashtable(&self) -> &ZendHashTable {
1✔
878
        // SAFETY: zend_empty_array is a static global initialized by PHP.
879
        unsafe { &zend_empty_array }
1✔
880
    }
1✔
881
}
882

883
impl IntoZval for ZendEmptyArray {
884
    const TYPE: DataType = DataType::Array;
885
    const NULLABLE: bool = false;
886

887
    fn set_zval(self, zv: &mut Zval, _persistent: bool) -> Result<()> {
1✔
888
        // Set the zval to point to the immutable shared empty array.
889
        // This mirrors the ZVAL_EMPTY_ARRAY macro in PHP.
890
        zv.u1.type_info = ZvalTypeFlags::Array.bits();
1✔
891
        zv.value.arr = ptr::from_ref(self.as_hashtable()).cast_mut();
1✔
892
        Ok(())
1✔
893
    }
1✔
894
}
895

896
#[cfg(test)]
897
#[cfg(feature = "embed")]
898
mod tests {
899
    use super::*;
900
    use crate::embed::Embed;
901

902
    #[test]
903
    fn test_has_key_string() {
1✔
904
        Embed::run(|| {
1✔
905
            let mut ht = ZendHashTable::new();
1✔
906
            let _ = ht.insert("test", "value");
1✔
907

908
            assert!(ht.has_key(&ArrayKey::from("test")));
1✔
909
            assert!(!ht.has_key(&ArrayKey::from("missing")));
1✔
910
        });
1✔
911
    }
1✔
912

913
    #[test]
914
    fn test_has_key_long() {
1✔
915
        Embed::run(|| {
1✔
916
            let mut ht = ZendHashTable::new();
1✔
917
            let _ = ht.push(42i64);
1✔
918

919
            assert!(ht.has_key(&ArrayKey::Long(0)));
1✔
920
            assert!(!ht.has_key(&ArrayKey::Long(1)));
1✔
921
        });
1✔
922
    }
1✔
923

924
    #[test]
925
    fn test_has_key_str_ref() {
1✔
926
        Embed::run(|| {
1✔
927
            let mut ht = ZendHashTable::new();
1✔
928
            let _ = ht.insert("hello", "world");
1✔
929

930
            let key = ArrayKey::Str("hello");
1✔
931
            assert!(ht.has_key(&key));
1✔
932
            // Key is still usable after has_key (no clone needed)
933
            assert!(ht.has_key(&key));
1✔
934

935
            assert!(!ht.has_key(&ArrayKey::Str("missing")));
1✔
936
        });
1✔
937
    }
1✔
938
}
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