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

extphprs / ext-php-rs / 30648853137

31 Jul 2026 04:51PM UTC coverage: 66.524% (+0.01%) from 66.511%
30648853137

push

github

web-flow
fix(types)!: stop deriving mutable references from shared borrows (#757)

14 of 48 new or added lines in 9 files covered. (29.17%)

10 existing lines in 4 files now uncovered.

8861 of 13320 relevant lines covered (66.52%)

33.2 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
×
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) }
×
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 {
×
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 mutable reference to a value in the hash table
215
    /// with a string key.
216
    ///
217
    /// # Parameters
218
    ///
219
    /// * `key` - The key to search for in the hash table.
220
    ///
221
    /// # Returns
222
    ///
223
    /// * `Some(&mut Zval)` - A mutable reference to the zval at the position in
224
    ///   the hash table.
225
    /// * `None` - No value at the given position was found.
226
    ///
227
    /// # Example
228
    ///
229
    /// ```no_run
230
    /// use ext_php_rs::types::ZendHashTable;
231
    ///
232
    /// let mut ht = ZendHashTable::new();
233
    ///
234
    /// ht.insert("test", "hello world");
235
    /// if let Some(zv) = ht.get_mut("test") {
236
    ///     zv.set_long(42);
237
    /// }
238
    /// assert_eq!(ht.get("test").and_then(|zv| zv.long()), Some(42));
239
    /// ```
240
    ///
241
    /// Holding two mutable references to the same entry is rejected at compile
242
    /// time, which is why this takes `&mut self`:
243
    ///
244
    /// ```compile_fail
245
    /// use ext_php_rs::types::ZendHashTable;
246
    ///
247
    /// let mut ht = ZendHashTable::new();
248
    /// let _ = ht.insert("test", 1);
249
    ///
250
    /// let first = ht.get_mut("test").unwrap();
251
    /// let second = ht.get_mut("test").unwrap();
252
    ///
253
    /// first.set_long(2);
254
    /// ```
255
    #[must_use]
NEW
256
    pub fn get_mut<'a, K>(&mut self, key: K) -> Option<&mut Zval>
×
257
    where
×
258
        K: Into<ArrayKey<'a>>,
×
259
    {
260
        match key.into() {
×
261
            ArrayKey::Long(index) => unsafe {
×
262
                #[allow(clippy::cast_sign_loss)]
263
                zend_hash_index_find(self, index as zend_ulong).as_mut()
×
264
            },
265
            ArrayKey::String(key) => unsafe {
×
266
                zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_mut()
×
267
            },
268
            ArrayKey::Str(key) => unsafe {
×
269
                zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_mut()
×
270
            },
271
            ArrayKey::ZendString(key) => unsafe {
×
272
                zend_hash_find(self, key.as_ptr().cast_mut()).as_mut()
×
273
            },
274
        }
275
    }
×
276

277
    /// Attempts to retrieve a value from the hash table with an index.
278
    ///
279
    /// # Parameters
280
    ///
281
    /// * `key` - The key to search for in the hash table.
282
    ///
283
    /// # Returns
284
    ///
285
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
286
    ///   table.
287
    /// * `None` - No value at the given position was found.
288
    ///
289
    /// # Example
290
    ///
291
    /// ```no_run
292
    /// use ext_php_rs::types::ZendHashTable;
293
    ///
294
    /// let mut ht = ZendHashTable::new();
295
    ///
296
    /// ht.push(100);
297
    /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
298
    /// ```
299
    #[must_use]
300
    pub fn get_index(&self, key: i64) -> Option<&Zval> {
2✔
301
        #[allow(clippy::cast_sign_loss)]
302
        unsafe {
303
            zend_hash_index_find(self, key as zend_ulong).as_ref()
2✔
304
        }
305
    }
2✔
306

307
    /// Attempts to retrieve a mutable reference to a value in the hash table
308
    /// with an index.
309
    ///
310
    /// # Parameters
311
    ///
312
    /// * `key` - The key to search for in the hash table.
313
    ///
314
    /// # Returns
315
    ///
316
    /// * `Some(&mut Zval)` - A mutable reference to the zval at the position in
317
    ///   the hash table.
318
    /// * `None` - No value at the given position was found.
319
    ///
320
    /// # Example
321
    ///
322
    /// ```no_run
323
    /// use ext_php_rs::types::ZendHashTable;
324
    ///
325
    /// let mut ht = ZendHashTable::new();
326
    ///
327
    /// ht.push(100);
328
    /// if let Some(zv) = ht.get_index_mut(0) {
329
    ///     zv.set_long(200);
330
    /// }
331
    /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(200));
332
    /// ```
333
    ///
334
    /// Holding two mutable references to the same index is rejected at compile
335
    /// time, which is why this takes `&mut self`:
336
    ///
337
    /// ```compile_fail
338
    /// use ext_php_rs::types::ZendHashTable;
339
    ///
340
    /// let mut ht = ZendHashTable::new();
341
    /// let _ = ht.push(100);
342
    ///
343
    /// let first = ht.get_index_mut(0).unwrap();
344
    /// let second = ht.get_index_mut(0).unwrap();
345
    ///
346
    /// first.set_long(200);
347
    /// ```
348
    #[must_use]
NEW
349
    pub fn get_index_mut(&mut self, key: i64) -> Option<&mut Zval> {
×
350
        unsafe {
351
            #[allow(clippy::cast_sign_loss)]
352
            zend_hash_index_find(self, key as zend_ulong).as_mut()
×
353
        }
354
    }
×
355

356
    /// Attempts to remove a value from the hash table with a string key.
357
    ///
358
    /// # Parameters
359
    ///
360
    /// * `key` - The key to remove from the hash table.
361
    ///
362
    /// # Returns
363
    ///
364
    /// * `Some(())` - Key was successfully removed.
365
    /// * `None` - No key was removed, did not exist.
366
    ///
367
    /// # Example
368
    ///
369
    /// ```no_run
370
    /// use ext_php_rs::types::ZendHashTable;
371
    ///
372
    /// let mut ht = ZendHashTable::new();
373
    ///
374
    /// ht.insert("test", "hello world");
375
    /// assert_eq!(ht.len(), 1);
376
    ///
377
    /// ht.remove("test");
378
    /// assert_eq!(ht.len(), 0);
379
    /// ```
380
    pub fn remove<'a, K>(&mut self, key: K) -> Option<()>
1✔
381
    where
1✔
382
        K: Into<ArrayKey<'a>>,
1✔
383
    {
384
        let result = match key.into() {
1✔
385
            ArrayKey::Long(index) => unsafe {
×
386
                #[allow(clippy::cast_sign_loss)]
387
                zend_hash_index_del(self, index as zend_ulong)
×
388
            },
389
            ArrayKey::String(key) => unsafe {
×
390
                zend_hash_str_del(self, key.as_ptr().cast(), key.len() as _)
×
391
            },
392
            ArrayKey::Str(key) => unsafe {
1✔
393
                zend_hash_str_del(self, key.as_ptr().cast(), key.len() as _)
1✔
394
            },
395
            ArrayKey::ZendString(key) => unsafe { zend_hash_del(self, key.as_ptr().cast_mut()) },
×
396
        };
397

398
        if result < 0 { None } else { Some(()) }
1✔
399
    }
1✔
400

401
    /// Attempts to remove a value from the hash table with a string key.
402
    ///
403
    /// # Parameters
404
    ///
405
    /// * `key` - The key to remove from the hash table.
406
    ///
407
    /// # Returns
408
    ///
409
    /// * `Ok(())` - Key was successfully removed.
410
    /// * `None` - No key was removed, did not exist.
411
    ///
412
    /// # Example
413
    ///
414
    /// ```no_run
415
    /// use ext_php_rs::types::ZendHashTable;
416
    ///
417
    /// let mut ht = ZendHashTable::new();
418
    ///
419
    /// ht.push("hello");
420
    /// assert_eq!(ht.len(), 1);
421
    ///
422
    /// ht.remove_index(0);
423
    /// assert_eq!(ht.len(), 0);
424
    /// ```
425
    pub fn remove_index(&mut self, key: i64) -> Option<()> {
×
426
        let result = unsafe {
×
427
            #[allow(clippy::cast_sign_loss)]
428
            zend_hash_index_del(self, key as zend_ulong)
×
429
        };
430

431
        if result < 0 { None } else { Some(()) }
×
432
    }
×
433

434
    /// Attempts to insert an item into the hash table, or update if the key
435
    /// already exists. Returns nothing in a result if successful.
436
    ///
437
    /// # Parameters
438
    ///
439
    /// * `key` - The key to insert the value at in the hash table.
440
    /// * `value` - The value to insert into the hash table.
441
    ///
442
    /// # Returns
443
    ///
444
    /// Returns nothing in a result on success.
445
    ///
446
    /// # Errors
447
    ///
448
    /// Returns an error if converting the value into a [`Zval`] failed.
449
    ///
450
    /// # Example
451
    ///
452
    /// ```no_run
453
    /// use ext_php_rs::types::ZendHashTable;
454
    ///
455
    /// let mut ht = ZendHashTable::new();
456
    ///
457
    /// ht.insert("a", "A");
458
    /// ht.insert("b", "B");
459
    /// ht.insert("c", "C");
460
    /// assert_eq!(ht.len(), 3);
461
    /// ```
462
    pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()>
190✔
463
    where
190✔
464
        K: Into<ArrayKey<'a>>,
190✔
465
        V: IntoZval,
190✔
466
    {
467
        let mut val = val.into_zval(false)?;
190✔
468
        match key.into() {
190✔
469
            ArrayKey::Long(index) => {
109✔
470
                unsafe {
109✔
471
                    #[allow(clippy::cast_sign_loss)]
109✔
472
                    zend_hash_index_update(self, index as zend_ulong, &raw mut val)
109✔
473
                };
109✔
474
            }
109✔
475
            ArrayKey::String(key) => {
7✔
476
                unsafe {
7✔
477
                    // Use raw bytes directly since zend_hash_str_update takes a length.
7✔
478
                    // This allows keys with embedded null bytes (e.g. PHP property mangling).
7✔
479
                    zend_hash_str_update(
7✔
480
                        self,
7✔
481
                        key.as_str().as_ptr().cast(),
7✔
482
                        key.len(),
7✔
483
                        &raw mut val,
7✔
484
                    )
7✔
485
                };
7✔
486
            }
7✔
487
            ArrayKey::Str(key) => {
73✔
488
                unsafe {
73✔
489
                    // Use raw bytes directly since zend_hash_str_update takes a length.
73✔
490
                    // This allows keys with embedded null bytes (e.g. PHP property mangling).
73✔
491
                    zend_hash_str_update(self, key.as_ptr().cast(), key.len(), &raw mut val)
73✔
492
                };
73✔
493
            }
73✔
494
            ArrayKey::ZendString(key) => {
1✔
495
                unsafe {
1✔
496
                    // zend_hash_update does the addref itself for non-interned strings.
1✔
497
                    zend_hash_update(self, key.as_ptr().cast_mut(), &raw mut val)
1✔
498
                };
1✔
499
            }
1✔
500
        }
501
        val.release();
190✔
502
        Ok(())
190✔
503
    }
190✔
504

505
    /// Inserts an item into the hash table at a specified index, or updates if
506
    /// the key already exists. Returns nothing in a result if successful.
507
    ///
508
    /// # Parameters
509
    ///
510
    /// * `key` - The index at which the value should be inserted.
511
    /// * `val` - The value to insert into the hash table.
512
    ///
513
    /// # Returns
514
    ///
515
    /// Returns nothing in a result on success.
516
    ///
517
    /// # Errors
518
    ///
519
    /// Returns an error if converting the value into a [`Zval`] failed.
520
    ///
521
    /// # Example
522
    ///
523
    /// ```no_run
524
    /// use ext_php_rs::types::ZendHashTable;
525
    ///
526
    /// let mut ht = ZendHashTable::new();
527
    ///
528
    /// ht.insert_at_index(0, "A");
529
    /// ht.insert_at_index(5, "B");
530
    /// ht.insert_at_index(0, "C"); // notice overriding index 0
531
    /// assert_eq!(ht.len(), 2);
532
    /// ```
533
    pub fn insert_at_index<V>(&mut self, key: i64, val: V) -> Result<()>
×
534
    where
×
535
        V: IntoZval,
×
536
    {
537
        let mut val = val.into_zval(false)?;
×
538
        unsafe {
539
            #[allow(clippy::cast_sign_loss)]
540
            zend_hash_index_update(self, key as zend_ulong, &raw mut val)
×
541
        };
542
        val.release();
×
543
        Ok(())
×
544
    }
×
545

546
    /// Pushes an item onto the end of the hash table. Returns a result
547
    /// containing nothing if the element was successfully inserted.
548
    ///
549
    /// # Parameters
550
    ///
551
    /// * `val` - The value to insert into the hash table.
552
    ///
553
    /// # Returns
554
    ///
555
    /// Returns nothing in a result on success.
556
    ///
557
    /// # Errors
558
    ///
559
    /// Returns an error if converting the value into a [`Zval`] failed.
560
    ///
561
    /// # Example
562
    ///
563
    /// ```no_run
564
    /// use ext_php_rs::types::ZendHashTable;
565
    ///
566
    /// let mut ht = ZendHashTable::new();
567
    ///
568
    /// ht.push("a");
569
    /// ht.push("b");
570
    /// ht.push("c");
571
    /// assert_eq!(ht.len(), 3);
572
    /// ```
573
    pub fn push<V>(&mut self, val: V) -> Result<()>
7✔
574
    where
7✔
575
        V: IntoZval,
7✔
576
    {
577
        let mut val = val.into_zval(false)?;
7✔
578
        unsafe { zend_hash_next_index_insert(self, &raw mut val) };
7✔
579
        val.release();
7✔
580

581
        Ok(())
7✔
582
    }
7✔
583

584
    /// Checks if the hashtable only contains numerical keys.
585
    ///
586
    /// # Returns
587
    ///
588
    /// True if all keys on the hashtable are numerical.
589
    ///
590
    /// # Example
591
    ///
592
    /// ```no_run
593
    /// use ext_php_rs::types::ZendHashTable;
594
    ///
595
    /// let mut ht = ZendHashTable::new();
596
    ///
597
    /// ht.push(0);
598
    /// ht.push(3);
599
    /// ht.push(9);
600
    /// assert!(ht.has_numerical_keys());
601
    ///
602
    /// ht.insert("obviously not numerical", 10);
603
    /// assert!(!ht.has_numerical_keys());
604
    /// ```
605
    #[must_use]
606
    pub fn has_numerical_keys(&self) -> bool {
×
607
        !self.into_iter().any(|(k, _)| !k.is_long())
×
608
    }
×
609

610
    /// Checks if the hashtable has numerical, sequential keys.
611
    ///
612
    /// # Returns
613
    ///
614
    /// True if all keys on the hashtable are numerical and are in sequential
615
    /// order (i.e. starting at 0 and not skipping any keys).
616
    ///
617
    /// # Panics
618
    ///
619
    /// Panics if the number of elements in the hashtable exceeds `i64::MAX`.
620
    ///
621
    /// # Example
622
    ///
623
    /// ```no_run
624
    /// use ext_php_rs::types::ZendHashTable;
625
    ///
626
    /// let mut ht = ZendHashTable::new();
627
    ///
628
    /// ht.push(0);
629
    /// ht.push(3);
630
    /// ht.push(9);
631
    /// assert!(ht.has_sequential_keys());
632
    ///
633
    /// ht.insert_at_index(90, 10);
634
    /// assert!(!ht.has_sequential_keys());
635
    /// ```
636
    #[must_use]
637
    pub fn has_sequential_keys(&self) -> bool {
×
638
        !self
×
639
            .into_iter()
×
640
            .enumerate()
×
641
            .any(|(i, (k, _))| ArrayKey::Long(i64::try_from(i).expect("Integer overflow")) != k)
×
642
    }
×
643

644
    /// Returns an iterator over the values contained inside the hashtable, as
645
    /// if it was a set or list.
646
    ///
647
    /// # Example
648
    ///
649
    /// ```no_run
650
    /// use ext_php_rs::types::ZendHashTable;
651
    ///
652
    /// let mut ht = ZendHashTable::new();
653
    ///
654
    /// for val in ht.values() {
655
    ///     dbg!(val);
656
    /// }
657
    #[inline]
658
    #[must_use]
659
    pub fn values(&self) -> Values<'_> {
×
660
        Values::new(self)
×
661
    }
×
662

663
    /// Returns an iterator over the key(s) and value contained inside the
664
    /// hashtable.
665
    ///
666
    /// # Example
667
    ///
668
    /// ```no_run
669
    /// use ext_php_rs::types::{ZendHashTable, ArrayKey};
670
    ///
671
    /// let mut ht = ZendHashTable::new();
672
    ///
673
    /// for (key, val) in ht.iter() {
674
    ///     match &key {
675
    ///         ArrayKey::Long(index) => {
676
    ///         }
677
    ///         ArrayKey::String(key) => {
678
    ///         }
679
    ///         ArrayKey::Str(key) => {
680
    ///         }
681
    ///         ArrayKey::ZendString(key) => {
682
    ///         }
683
    ///     }
684
    ///     dbg!(key, val);
685
    /// }
686
    #[inline]
687
    #[must_use]
688
    pub fn iter(&self) -> Iter<'_> {
2✔
689
        self.into_iter()
2✔
690
    }
2✔
691

692
    /// Gets the given key's corresponding entry in the hashtable for in-place
693
    /// manipulation.
694
    ///
695
    /// This API is similar to Rust's [`std::collections::hash_map::HashMap::entry`].
696
    ///
697
    /// # Parameters
698
    ///
699
    /// * `key` - The key to look up in the hashtable.
700
    ///
701
    /// # Returns
702
    ///
703
    /// An `Entry` enum that can be used to insert or modify the value at
704
    /// the given key.
705
    ///
706
    /// # Example
707
    ///
708
    /// ```no_run
709
    /// use ext_php_rs::types::ZendHashTable;
710
    ///
711
    /// let mut ht = ZendHashTable::new();
712
    ///
713
    /// // Insert a default value if the key doesn't exist
714
    /// ht.entry("counter").or_insert(0i64);
715
    ///
716
    /// // Modify the value if it exists
717
    /// ht.entry("counter").and_modify(|v| {
718
    ///     if let Some(n) = v.long() {
719
    ///         v.set_long(n + 1);
720
    ///     }
721
    /// });
722
    ///
723
    /// // Use or_insert_with for lazy initialization
724
    /// ht.entry("computed").or_insert_with(|| "computed value");
725
    ///
726
    /// // Works with numeric keys too
727
    /// ht.entry(42i64).or_insert("value at index 42");
728
    /// ```
729
    #[must_use]
730
    pub fn entry<'a, 'k, K>(&'a mut self, key: K) -> Entry<'a, 'k>
11✔
731
    where
11✔
732
        K: Into<ArrayKey<'k>>,
11✔
733
    {
734
        let key = key.into();
11✔
735
        if self.has_key(&key) {
11✔
736
            Entry::Occupied(OccupiedEntry::new(self, key))
4✔
737
        } else {
738
            Entry::Vacant(VacantEntry::new(self, key))
7✔
739
        }
740
    }
11✔
741

742
    /// Checks if a key exists in the hash table.
743
    ///
744
    /// # Parameters
745
    ///
746
    /// * `key` - The key to check for in the hash table.
747
    ///
748
    /// # Returns
749
    ///
750
    /// * `true` - The key exists in the hash table.
751
    /// * `false` - The key does not exist in the hash table.
752
    ///
753
    /// # Example
754
    ///
755
    /// ```no_run
756
    /// use ext_php_rs::types::{ZendHashTable, ArrayKey};
757
    ///
758
    /// let mut ht = ZendHashTable::new();
759
    ///
760
    /// ht.insert("test", "hello world");
761
    /// assert!(ht.has_key(&ArrayKey::from("test")));
762
    /// assert!(!ht.has_key(&ArrayKey::from("missing")));
763
    /// ```
764
    #[must_use]
765
    pub fn has_key(&self, key: &ArrayKey<'_>) -> bool {
20✔
766
        match key {
20✔
767
            ArrayKey::Long(index) => unsafe {
4✔
768
                #[allow(clippy::cast_sign_loss)]
769
                !zend_hash_index_find(self, *index as zend_ulong).is_null()
4✔
770
            },
771
            ArrayKey::String(key) => unsafe {
×
772
                !zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).is_null()
×
773
            },
774
            ArrayKey::Str(key) => unsafe {
14✔
775
                !zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).is_null()
14✔
776
            },
777
            ArrayKey::ZendString(key) => unsafe {
2✔
778
                !zend_hash_find(self, key.as_ptr().cast_mut()).is_null()
2✔
779
            },
780
        }
781
    }
20✔
782

783
    /// Determines whether this hashtable is immutable.
784
    ///
785
    /// Immutable hashtables are shared and cannot be modified. The primary
786
    /// example is the empty immutable shared array returned by
787
    /// [`ZendEmptyArray`].
788
    ///
789
    /// # Example
790
    ///
791
    /// ```no_run
792
    /// use ext_php_rs::types::ZendHashTable;
793
    ///
794
    /// let ht = ZendHashTable::new();
795
    /// assert!(!ht.is_immutable());
796
    /// ```
797
    #[must_use]
798
    pub fn is_immutable(&self) -> bool {
92✔
799
        // SAFETY: Type info is initialized by Zend on array init.
800
        let gc_type_info = unsafe { self.gc.u.type_info };
92✔
801
        let gc_flags = (gc_type_info >> GC_FLAGS_SHIFT) & (GC_FLAGS_MASK >> GC_FLAGS_SHIFT);
92✔
802

803
        gc_flags & ZvalTypeFlags::Immutable.bits() != 0
92✔
804
    }
92✔
805
}
806

807
unsafe impl ZBoxable for ZendHashTable {
808
    fn free(&mut self) {
48✔
809
        // Do not attempt to free the immutable shared empty array.
810
        if self.is_immutable() {
48✔
811
            return;
×
812
        }
48✔
813
        // SAFETY: ZBox has immutable access to `self`.
814
        unsafe { zend_array_destroy(self) }
48✔
815
    }
48✔
816
}
817

818
impl Debug for ZendHashTable {
819
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
820
        f.debug_map()
×
821
            .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
×
822
            .finish()
×
823
    }
×
824
}
825

826
impl ToOwned for ZendHashTable {
827
    type Owned = ZBox<ZendHashTable>;
828

829
    fn to_owned(&self) -> Self::Owned {
×
830
        unsafe {
831
            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
832
            let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
×
833

834
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
835
            ZBox::from_raw(
×
836
                ptr.as_mut()
×
837
                    .expect("Failed to allocate memory for hashtable"),
×
838
            )
839
        }
840
    }
×
841
}
842

843
impl Default for ZBox<ZendHashTable> {
844
    fn default() -> Self {
×
845
        ZendHashTable::new()
×
846
    }
×
847
}
848

849
impl Clone for ZBox<ZendHashTable> {
850
    fn clone(&self) -> Self {
×
851
        (**self).to_owned()
×
852
    }
×
853
}
854

855
impl IntoZval for ZBox<ZendHashTable> {
856
    const TYPE: DataType = DataType::Array;
857
    const NULLABLE: bool = false;
858

859
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1✔
860
        zv.set_hashtable(self);
1✔
861
        Ok(())
1✔
862
    }
1✔
863
}
864

865
impl<'a> FromZval<'a> for &'a ZendHashTable {
866
    const TYPE: DataType = DataType::Array;
867

868
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
869
        zval.array()
×
870
    }
×
871
}
872

873
impl<'a> FromZvalMut<'a> for &'a mut ZendHashTable {
874
    const TYPE: DataType = DataType::Array;
875

876
    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
×
877
        zval.array_mut()
×
878
    }
×
879
}
880

881
/// Represents an empty, immutable, shared PHP array.
882
///
883
/// Since PHP 7.3, it's possible for extensions to return a zval backed by
884
/// an immutable shared hashtable. This helps avoid redundant hashtable
885
/// allocations when returning empty arrays to userland PHP code.
886
///
887
/// This struct provides a safe way to return an empty array without allocating
888
/// a new hashtable. It implements [`IntoZval`] so it can be used as a return
889
/// type for PHP functions.
890
///
891
/// # Safety
892
///
893
/// Unlike [`ZendHashTable`], this type does not allow any mutation of the
894
/// underlying array, as it points to a shared static empty array in PHP's
895
/// memory.
896
///
897
/// # Example
898
///
899
/// ```rust,ignore
900
/// use ext_php_rs::prelude::*;
901
/// use ext_php_rs::types::ZendEmptyArray;
902
///
903
/// #[php_function]
904
/// pub fn get_empty_array() -> ZendEmptyArray {
905
///     ZendEmptyArray
906
/// }
907
/// ```
908
///
909
/// This is more efficient than returning `Vec::<i32>::new()` or creating
910
/// a new `ZendHashTable` when you know the result will be empty.
911
#[derive(Debug, Clone, Copy, Default)]
912
pub struct ZendEmptyArray;
913

914
impl ZendEmptyArray {
915
    /// Returns a reference to the underlying immutable empty hashtable.
916
    ///
917
    /// # Example
918
    ///
919
    /// ```no_run
920
    /// use ext_php_rs::types::ZendEmptyArray;
921
    ///
922
    /// let empty = ZendEmptyArray;
923
    /// let ht = empty.as_hashtable();
924
    /// assert!(ht.is_empty());
925
    /// assert!(ht.is_immutable());
926
    /// ```
927
    #[must_use]
928
    pub fn as_hashtable(&self) -> &ZendHashTable {
1✔
929
        // SAFETY: zend_empty_array is a static global initialized by PHP.
930
        unsafe { &zend_empty_array }
1✔
931
    }
1✔
932
}
933

934
impl IntoZval for ZendEmptyArray {
935
    const TYPE: DataType = DataType::Array;
936
    const NULLABLE: bool = false;
937

938
    fn set_zval(self, zv: &mut Zval, _persistent: bool) -> Result<()> {
1✔
939
        // Set the zval to point to the immutable shared empty array.
940
        // This mirrors the ZVAL_EMPTY_ARRAY macro in PHP.
941
        zv.u1.type_info = ZvalTypeFlags::Array.bits();
1✔
942
        zv.value.arr = ptr::from_ref(self.as_hashtable()).cast_mut();
1✔
943
        Ok(())
1✔
944
    }
1✔
945
}
946

947
#[cfg(test)]
948
#[cfg(feature = "embed")]
949
mod tests {
950
    use super::*;
951
    use crate::embed::Embed;
952
    use crate::types::ZendStr;
953

954
    #[test]
955
    fn test_has_key_string() {
1✔
956
        Embed::run(|| {
1✔
957
            let mut ht = ZendHashTable::new();
1✔
958
            let _ = ht.insert("test", "value");
1✔
959

960
            assert!(ht.has_key(&ArrayKey::from("test")));
1✔
961
            assert!(!ht.has_key(&ArrayKey::from("missing")));
1✔
962
        });
1✔
963
    }
1✔
964

965
    #[test]
966
    fn test_has_key_long() {
1✔
967
        Embed::run(|| {
1✔
968
            let mut ht = ZendHashTable::new();
1✔
969
            let _ = ht.push(42i64);
1✔
970

971
            assert!(ht.has_key(&ArrayKey::Long(0)));
1✔
972
            assert!(!ht.has_key(&ArrayKey::Long(1)));
1✔
973
        });
1✔
974
    }
1✔
975

976
    #[test]
977
    fn test_has_key_str_ref() {
1✔
978
        Embed::run(|| {
1✔
979
            let mut ht = ZendHashTable::new();
1✔
980
            let _ = ht.insert("hello", "world");
1✔
981

982
            let key = ArrayKey::Str("hello");
1✔
983
            assert!(ht.has_key(&key));
1✔
984
            // Key is still usable after has_key (no clone needed)
985
            assert!(ht.has_key(&key));
1✔
986

987
            assert!(!ht.has_key(&ArrayKey::Str("missing")));
1✔
988
        });
1✔
989
    }
1✔
990

991
    #[test]
992
    fn test_has_key_zend_string() {
1✔
993
        Embed::run(|| {
1✔
994
            let mut ht = ZendHashTable::new();
1✔
995
            let key = ZendStr::new("hello", false);
1✔
996

997
            let _ = ht.insert(&key, "world");
1✔
998
            assert!(ht.has_key(&ArrayKey::ZendString(&key)));
1✔
999
        });
1✔
1000
    }
1✔
1001

1002
    #[test]
1003
    fn test_zend_string_numeric_key_normalizes_to_long() {
1✔
1004
        Embed::run(|| {
1✔
1005
            let mut ht = ZendHashTable::new();
1✔
1006
            let numeric_key = ZendStr::new("42", false);
1✔
1007

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

1010
            assert_eq!(ht.get_index(42).and_then(|v| v.str()), Some("value"));
1✔
1011
            assert_eq!(ht.get("42").and_then(|v| v.str()), Some("value"));
1✔
1012
            assert_eq!(ht.get(&numeric_key).and_then(|v| v.str()), Some("value"));
1✔
1013
            assert!(ht.has_key(&ArrayKey::from(&numeric_key)));
1✔
1014
        });
1✔
1015
    }
1✔
1016
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc