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

extphprs / ext-php-rs / 20542278985

27 Dec 2025 05:41PM UTC coverage: 36.351% (+0.03%) from 36.32%
20542278985

push

github

web-flow
feat: add support for empty immutable shared arrays

Refs: #631, #355

8 of 16 new or added lines in 2 files covered. (50.0%)

1811 of 4982 relevant lines covered (36.35%)

13.98 hits per line

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

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

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

6
use crate::{
7
    boxed::{ZBox, ZBoxable},
8
    convert::{FromZval, 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 iterators;
24

25
pub use array_key::ArrayKey;
26
pub use iterators::{Iter, Values};
27

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

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

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

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

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

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

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

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

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

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

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

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

370
        if result < 0 { None } else { Some(()) }
×
371
    }
372

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

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

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

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

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

544
        Ok(())
×
545
    }
546

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

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

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

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

653
    /// Determines whether this hashtable is immutable.
654
    ///
655
    /// Immutable hashtables are shared and cannot be modified. The primary
656
    /// example is the empty immutable shared array returned by
657
    /// [`ZendEmptyArray`].
658
    ///
659
    /// # Example
660
    ///
661
    /// ```no_run
662
    /// use ext_php_rs::types::ZendHashTable;
663
    ///
664
    /// let ht = ZendHashTable::new();
665
    /// assert!(!ht.is_immutable());
666
    /// ```
667
    #[must_use]
668
    pub fn is_immutable(&self) -> bool {
44✔
669
        // SAFETY: Type info is initialized by Zend on array init.
670
        let gc_type_info = unsafe { self.gc.u.type_info };
88✔
671
        let gc_flags = (gc_type_info >> GC_FLAGS_SHIFT) & (GC_FLAGS_MASK >> GC_FLAGS_SHIFT);
88✔
672

673
        gc_flags & ZvalTypeFlags::Immutable.bits() != 0
88✔
674
    }
675
}
676

677
unsafe impl ZBoxable for ZendHashTable {
678
    fn free(&mut self) {
23✔
679
        // Do not attempt to free the immutable shared empty array.
680
        if self.is_immutable() {
46✔
NEW
681
            return;
×
682
        }
683
        // SAFETY: ZBox has immutable access to `self`.
684
        unsafe { zend_array_destroy(self) }
46✔
685
    }
686
}
687

688
impl Debug for ZendHashTable {
689
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
690
        f.debug_map()
×
691
            .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
×
692
            .finish()
693
    }
694
}
695

696
impl ToOwned for ZendHashTable {
697
    type Owned = ZBox<ZendHashTable>;
698

699
    fn to_owned(&self) -> Self::Owned {
×
700
        unsafe {
701
            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
702
            let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
×
703

704
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
705
            ZBox::from_raw(
706
                ptr.as_mut()
×
707
                    .expect("Failed to allocate memory for hashtable"),
×
708
            )
709
        }
710
    }
711
}
712

713
impl Default for ZBox<ZendHashTable> {
714
    fn default() -> Self {
×
715
        ZendHashTable::new()
×
716
    }
717
}
718

719
impl Clone for ZBox<ZendHashTable> {
720
    fn clone(&self) -> Self {
×
721
        (**self).to_owned()
×
722
    }
723
}
724

725
impl IntoZval for ZBox<ZendHashTable> {
726
    const TYPE: DataType = DataType::Array;
727
    const NULLABLE: bool = false;
728

729
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
730
        zv.set_hashtable(self);
×
731
        Ok(())
×
732
    }
733
}
734

735
impl<'a> FromZval<'a> for &'a ZendHashTable {
736
    const TYPE: DataType = DataType::Array;
737

738
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
739
        zval.array()
×
740
    }
741
}
742

743
impl<'a> FromZvalMut<'a> for &'a mut ZendHashTable {
744
    const TYPE: DataType = DataType::Array;
745

746
    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
×
747
        zval.array_mut()
×
748
    }
749
}
750

751
/// Represents an empty, immutable, shared PHP array.
752
///
753
/// Since PHP 7.3, it's possible for extensions to return a zval backed by
754
/// an immutable shared hashtable. This helps avoid redundant hashtable
755
/// allocations when returning empty arrays to userland PHP code.
756
///
757
/// This struct provides a safe way to return an empty array without allocating
758
/// a new hashtable. It implements [`IntoZval`] so it can be used as a return
759
/// type for PHP functions.
760
///
761
/// # Safety
762
///
763
/// Unlike [`ZendHashTable`], this type does not allow any mutation of the
764
/// underlying array, as it points to a shared static empty array in PHP's
765
/// memory.
766
///
767
/// # Example
768
///
769
/// ```rust,ignore
770
/// use ext_php_rs::prelude::*;
771
/// use ext_php_rs::types::ZendEmptyArray;
772
///
773
/// #[php_function]
774
/// pub fn get_empty_array() -> ZendEmptyArray {
775
///     ZendEmptyArray
776
/// }
777
/// ```
778
///
779
/// This is more efficient than returning `Vec::<i32>::new()` or creating
780
/// a new `ZendHashTable` when you know the result will be empty.
781
#[derive(Debug, Clone, Copy, Default)]
782
pub struct ZendEmptyArray;
783

784
impl ZendEmptyArray {
785
    /// Returns a reference to the underlying immutable empty hashtable.
786
    ///
787
    /// # Example
788
    ///
789
    /// ```no_run
790
    /// use ext_php_rs::types::ZendEmptyArray;
791
    ///
792
    /// let empty = ZendEmptyArray;
793
    /// let ht = empty.as_hashtable();
794
    /// assert!(ht.is_empty());
795
    /// assert!(ht.is_immutable());
796
    /// ```
797
    #[must_use]
NEW
798
    pub fn as_hashtable(&self) -> &ZendHashTable {
×
799
        // SAFETY: zend_empty_array is a static global initialized by PHP.
NEW
800
        unsafe { &zend_empty_array }
×
801
    }
802
}
803

804
impl IntoZval for ZendEmptyArray {
805
    const TYPE: DataType = DataType::Array;
806
    const NULLABLE: bool = false;
807

NEW
808
    fn set_zval(self, zv: &mut Zval, _persistent: bool) -> Result<()> {
×
809
        // Set the zval to point to the immutable shared empty array.
810
        // This mirrors the ZVAL_EMPTY_ARRAY macro in PHP.
NEW
811
        zv.u1.type_info = ZvalTypeFlags::Array.bits();
×
NEW
812
        zv.value.arr = ptr::from_ref(self.as_hashtable()).cast_mut();
×
NEW
813
        Ok(())
×
814
    }
815
}
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