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

extphprs / ext-php-rs / 23743444734

30 Mar 2026 11:55AM UTC coverage: 65.794% (-0.3%) from 66.102%
23743444734

Pull #707

github

web-flow
Merge 87b7f8bab into 030380f14
Pull Request #707: fix(memory): eliminate module definition memory leak

6 of 62 new or added lines in 1 file covered. (9.68%)

494 existing lines in 15 files now uncovered.

8117 of 12337 relevant lines covered (65.79%)

32.73 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 {
105✔
126
        unsafe { zend_array_count(ptr::from_ref(self).cast_mut()) as usize }
105✔
127
    }
105✔
128

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

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

169
    /// Attempts to retrieve a value from the hash table with a string key.
170
    ///
171
    /// # Parameters
172
    ///
173
    /// * `key` - The key to search for in the hash table.
174
    ///
175
    /// # Returns
176
    ///
177
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
178
    ///   table.
179
    /// * `None` - No value at the given position was found.
180
    ///
181
    /// # Example
182
    ///
183
    /// ```no_run
184
    /// use ext_php_rs::types::ZendHashTable;
185
    ///
186
    /// let mut ht = ZendHashTable::new();
187
    ///
188
    /// ht.insert("test", "hello world");
189
    /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
190
    /// ```
191
    #[must_use]
192
    pub fn get<'a, K>(&self, key: K) -> Option<&Zval>
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<()>
7✔
528
    where
7✔
529
        V: IntoZval,
7✔
530
    {
531
        let mut val = val.into_zval(false)?;
7✔
532
        unsafe { zend_hash_next_index_insert(self, &raw mut val) };
7✔
533
        val.release();
7✔
534

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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