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

extphprs / ext-php-rs / 26832618561

02 Jun 2026 04:11PM UTC coverage: 72.397% (-0.08%) from 72.479%
26832618561

push

github

ptondereau
fix(tests): wrap PHP function handlers in zend_fastcall! for windows

FunctionHandler resolves to extern "C" on unix and to
extern "vectorcall" on windows. The new compound-type integration
handlers (class union, intersection, DNF, primitive union) and the
src/builders/function.rs noop_handler test helper were declared
as plain extern "C", which only matches the unix alias and so
failed to type-check on windows with E0308.

Wrap each handler in zend_fastcall! { ... }, the same macro
closure.rs and builders/class.rs already use. The macro rewrites
the ABI to vectorcall on windows and stays C on unix.

Also gates the noop_handler helper behind cfg(php83) since the
tests that consume it are all PHP 8.3+ (class union, intersection,
DNF return types).

Drops a redundant `#![cfg_attr(windows, feature(abi_vectorcall))]`
attribute inside src/describe/mod.rs's tests module: inner
`feature` attributes only take effect at the crate root, so it was
a no-op that produced "the `#![feature]` attribute can only be
used at the crate root" on every windows compile. The crate root
already enables the feature in src/lib.rs.

0 of 1 new or added line in 1 file covered. (0.0%)

257 existing lines in 10 files now uncovered.

11564 of 15973 relevant lines covered (72.4%)

33.46 hits per line

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

67.87
/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_del,
14
        zend_hash_find, zend_hash_index_del, zend_hash_index_find, zend_hash_index_update,
15
        zend_hash_next_index_insert, zend_hash_str_del, zend_hash_str_find, zend_hash_str_update,
16
        zend_hash_update,
17
    },
18
    flags::{DataType, ZvalTypeFlags},
19
    types::Zval,
20
};
21

22
mod array_key;
23
mod conversions;
24
mod entry;
25
mod iterators;
26

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

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

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

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

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

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

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

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

170
    /// Attempts to retrieve a value from the hash table with a string key.
171
    ///
172
    /// # Parameters
173
    ///
174
    /// * `key` - The key to search for in the hash table.
175
    ///
176
    /// # Returns
177
    ///
178
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
179
    ///   table.
180
    /// * `None` - No value at the given position was found.
181
    ///
182
    /// # Example
183
    ///
184
    /// ```no_run
185
    /// use ext_php_rs::types::ZendHashTable;
186
    ///
187
    /// let mut ht = ZendHashTable::new();
188
    ///
189
    /// ht.insert("test", "hello world");
190
    /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
191
    /// ```
192
    #[must_use]
193
    pub fn get<'a, K>(&self, key: K) -> Option<&Zval>
66✔
194
    where
66✔
195
        K: Into<ArrayKey<'a>>,
66✔
196
    {
197
        match key.into() {
66✔
198
            ArrayKey::Long(index) => unsafe {
34✔
199
                #[allow(clippy::cast_sign_loss)]
200
                zend_hash_index_find(self, index as zend_ulong).as_ref()
34✔
201
            },
202
            ArrayKey::String(key) => unsafe {
×
UNCOV
203
                zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_ref()
×
204
            },
205
            ArrayKey::Str(key) => unsafe {
31✔
206
                zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_ref()
31✔
207
            },
208
            ArrayKey::ZendString(key) => unsafe {
1✔
209
                zend_hash_find(self, key.as_ptr().cast_mut()).as_ref()
1✔
210
            },
211
        }
212
    }
66✔
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]
240
    pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval>
×
241
    where
×
UNCOV
242
        K: Into<ArrayKey<'a>>,
×
243
    {
UNCOV
244
        match key.into() {
×
245
            ArrayKey::Long(index) => unsafe {
×
246
                #[allow(clippy::cast_sign_loss)]
UNCOV
247
                zend_hash_index_find(self, index as zend_ulong).as_mut()
×
248
            },
249
            ArrayKey::String(key) => unsafe {
×
UNCOV
250
                zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_mut()
×
251
            },
252
            ArrayKey::Str(key) => unsafe {
×
UNCOV
253
                zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_mut()
×
254
            },
UNCOV
255
            ArrayKey::ZendString(key) => unsafe {
×
UNCOV
256
                zend_hash_find(self, key.as_ptr().cast_mut()).as_mut()
×
257
            },
258
        }
UNCOV
259
    }
×
260

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

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

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

366
        if result < 0 { None } else { Some(()) }
1✔
367
    }
1✔
368

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

UNCOV
399
        if result < 0 { None } else { Some(()) }
×
UNCOV
400
    }
×
401

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

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

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

549
        Ok(())
7✔
550
    }
7✔
551

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

955
            assert!(!ht.has_key(&ArrayKey::Str("missing")));
1✔
956
        });
1✔
957
    }
1✔
958

959
    #[test]
960
    fn test_has_key_zend_string() {
1✔
961
        Embed::run(|| {
1✔
962
            let mut ht = ZendHashTable::new();
1✔
963
            let key = ZendStr::new("hello", false);
1✔
964

965
            let _ = ht.insert(&key, "world");
1✔
966
            assert!(ht.has_key(&ArrayKey::ZendString(&key)));
1✔
967
        });
1✔
968
    }
1✔
969

970
    #[test]
971
    fn test_zend_string_numeric_key_normalizes_to_long() {
1✔
972
        Embed::run(|| {
1✔
973
            let mut ht = ZendHashTable::new();
1✔
974
            let numeric_key = ZendStr::new("42", false);
1✔
975

976
            let _ = ht.insert(&numeric_key, "value");
1✔
977

978
            assert_eq!(ht.get_index(42).and_then(|v| v.str()), Some("value"));
1✔
979
            assert_eq!(ht.get("42").and_then(|v| v.str()), Some("value"));
1✔
980
            assert_eq!(ht.get(&numeric_key).and_then(|v| v.str()), Some("value"));
1✔
981
            assert!(ht.has_key(&ArrayKey::from(&numeric_key)));
1✔
982
        });
1✔
983
    }
1✔
984
}
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