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

extphprs / ext-php-rs / 21328906419

25 Jan 2026 07:24AM UTC coverage: 34.533% (-1.5%) from 36.059%
21328906419

Pull #630

github

web-flow
Merge 514a48ff9 into 28bd50d8c
Pull Request #630: Draft: feat(callable): callable_channel #191

29 of 153 new or added lines in 1 file covered. (18.95%)

1240 existing lines in 27 files now uncovered.

1852 of 5363 relevant lines covered (34.53%)

13.69 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 {
×
UNCOV
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) {
×
UNCOV
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(),
×
UNCOV
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]
UNCOV
239
    pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval>
×
240
    where
241
        K: Into<ArrayKey<'a>>,
242
    {
UNCOV
243
        match key.into() {
×
244
            ArrayKey::Long(index) => unsafe {
245
                #[allow(clippy::cast_sign_loss)]
UNCOV
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(),
×
UNCOV
252
                    key.len() as _,
×
253
                )
254
                .as_mut()
255
            },
256
            ArrayKey::Str(key) => unsafe {
UNCOV
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]
UNCOV
285
    pub fn get_index(&self, key: i64) -> Option<&Zval> {
×
286
        #[allow(clippy::cast_sign_loss)]
287
        unsafe {
UNCOV
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]
UNCOV
318
    pub fn get_index_mut(&self, key: i64) -> Option<&mut Zval> {
×
319
        unsafe {
320
            #[allow(clippy::cast_sign_loss)]
UNCOV
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
    /// ```
UNCOV
349
    pub fn remove<'a, K>(&mut self, key: K) -> Option<()>
×
350
    where
351
        K: Into<ArrayKey<'a>>,
352
    {
UNCOV
353
        let result = match key.into() {
×
354
            ArrayKey::Long(index) => unsafe {
355
                #[allow(clippy::cast_sign_loss)]
UNCOV
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(),
×
UNCOV
362
                    key.len() as _,
×
363
                )
364
            },
365
            ArrayKey::Str(key) => unsafe {
UNCOV
366
                zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
×
367
            },
368
        };
369

UNCOV
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
    /// ```
UNCOV
397
    pub fn remove_index(&mut self, key: i64) -> Option<()> {
×
398
        let result = unsafe {
399
            #[allow(clippy::cast_sign_loss)]
UNCOV
400
            zend_hash_index_del(self, key as zend_ulong)
×
401
        };
402

UNCOV
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
            }
UNCOV
448
            ArrayKey::String(key) => {
×
449
                unsafe {
450
                    // Use raw bytes directly since zend_hash_str_update takes a length.
451
                    // This allows keys with embedded null bytes (e.g. PHP property mangling).
452
                    zend_hash_str_update(
453
                        self,
×
UNCOV
454
                        key.as_str().as_ptr().cast(),
×
UNCOV
455
                        key.len(),
×
UNCOV
456
                        &raw mut val,
×
457
                    )
458
                };
459
            }
460
            ArrayKey::Str(key) => {
96✔
461
                unsafe {
462
                    // Use raw bytes directly since zend_hash_str_update takes a length.
463
                    // This allows keys with embedded null bytes (e.g. PHP property mangling).
464
                    zend_hash_str_update(self, key.as_ptr().cast(), key.len(), &raw mut val)
336✔
465
                };
466
            }
467
        }
468
        val.release();
226✔
469
        Ok(())
113✔
470
    }
471

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

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

UNCOV
548
        Ok(())
×
549
    }
550

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

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

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

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

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

677
        gc_flags & ZvalTypeFlags::Immutable.bits() != 0
88✔
678
    }
679
}
680

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

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

700
impl ToOwned for ZendHashTable {
701
    type Owned = ZBox<ZendHashTable>;
702

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

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

717
impl Default for ZBox<ZendHashTable> {
718
    fn default() -> Self {
×
719
        ZendHashTable::new()
×
720
    }
721
}
722

723
impl Clone for ZBox<ZendHashTable> {
UNCOV
724
    fn clone(&self) -> Self {
×
UNCOV
725
        (**self).to_owned()
×
726
    }
727
}
728

729
impl IntoZval for ZBox<ZendHashTable> {
730
    const TYPE: DataType = DataType::Array;
731
    const NULLABLE: bool = false;
732

UNCOV
733
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
UNCOV
734
        zv.set_hashtable(self);
×
UNCOV
735
        Ok(())
×
736
    }
737
}
738

739
impl<'a> FromZval<'a> for &'a ZendHashTable {
740
    const TYPE: DataType = DataType::Array;
741

UNCOV
742
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
743
        zval.array()
×
744
    }
745
}
746

747
impl<'a> FromZvalMut<'a> for &'a mut ZendHashTable {
748
    const TYPE: DataType = DataType::Array;
749

UNCOV
750
    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
×
UNCOV
751
        zval.array_mut()
×
752
    }
753
}
754

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

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

808
impl IntoZval for ZendEmptyArray {
809
    const TYPE: DataType = DataType::Array;
810
    const NULLABLE: bool = false;
811

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