ext_php_rs/types/array.rs
1//! Represents an array in PHP. As all arrays in PHP are associative arrays,
2//! they are represented by hash tables.
3
4use std::{
5 collections::HashMap,
6 convert::{TryFrom, TryInto},
7 ffi::CString,
8 fmt::{Debug, Display},
9 iter::FromIterator,
10 ptr,
11 str::FromStr,
12};
13
14use crate::{
15 boxed::{ZBox, ZBoxable},
16 convert::{FromZval, IntoZval},
17 error::{Error, Result},
18 ffi::zend_ulong,
19 ffi::{
20 _zend_new_array, zend_array_count, zend_array_destroy, zend_array_dup, zend_hash_clean,
21 zend_hash_get_current_data_ex, zend_hash_get_current_key_type_ex,
22 zend_hash_get_current_key_zval_ex, zend_hash_index_del, zend_hash_index_find,
23 zend_hash_index_update, zend_hash_move_backwards_ex, zend_hash_move_forward_ex,
24 zend_hash_next_index_insert, zend_hash_str_del, zend_hash_str_find, zend_hash_str_update,
25 HashPosition, HT_MIN_SIZE,
26 },
27 flags::DataType,
28 types::Zval,
29};
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/// ```
54pub 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)]
59impl 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> {
75 Self::with_capacity(HT_MIN_SIZE)
76 }
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> {
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);
102
103 // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
104 ZBox::from_raw(
105 ptr.as_mut()
106 .expect("Failed to allocate memory for hashtable"),
107 )
108 }
109 }
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 {
127 unsafe { zend_array_count(ptr::from_ref(self).cast_mut()) as usize }
128 }
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>
194 where
195 K: Into<ArrayKey<'a>>,
196 {
197 match key.into() {
198 ArrayKey::Long(index) => unsafe {
199 #[allow(clippy::cast_sign_loss)]
200 zend_hash_index_find(self, index as zend_ulong).as_ref()
201 },
202 ArrayKey::String(key) => {
203 if let Ok(index) = i64::from_str(key.as_str()) {
204 #[allow(clippy::cast_sign_loss)]
205 unsafe {
206 zend_hash_index_find(self, index as zend_ulong).as_ref()
207 }
208 } else {
209 unsafe {
210 zend_hash_str_find(
211 self,
212 CString::new(key.as_str()).ok()?.as_ptr(),
213 key.len() as _,
214 )
215 .as_ref()
216 }
217 }
218 }
219 ArrayKey::Str(key) => {
220 if let Ok(index) = i64::from_str(key) {
221 #[allow(clippy::cast_sign_loss)]
222 unsafe {
223 zend_hash_index_find(self, index as zend_ulong).as_ref()
224 }
225 } else {
226 unsafe {
227 zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
228 .as_ref()
229 }
230 }
231 }
232 }
233 }
234
235 /// Attempts to retrieve a value from the hash table with a string key.
236 ///
237 /// # Parameters
238 ///
239 /// * `key` - The key to search for in the hash table.
240 ///
241 /// # Returns
242 ///
243 /// * `Some(&Zval)` - A reference to the zval at the position in the hash
244 /// table.
245 /// * `None` - No value at the given position was found.
246 ///
247 /// # Example
248 ///
249 /// ```no_run
250 /// use ext_php_rs::types::ZendHashTable;
251 ///
252 /// let mut ht = ZendHashTable::new();
253 ///
254 /// ht.insert("test", "hello world");
255 /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
256 /// ```
257 // TODO: Verify if this is safe to use, as it allows mutating the
258 // hashtable while only having a reference to it. #461
259 #[allow(clippy::mut_from_ref)]
260 #[must_use]
261 pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval>
262 where
263 K: Into<ArrayKey<'a>>,
264 {
265 match key.into() {
266 ArrayKey::Long(index) => unsafe {
267 #[allow(clippy::cast_sign_loss)]
268 zend_hash_index_find(self, index as zend_ulong).as_mut()
269 },
270 ArrayKey::String(key) => {
271 if let Ok(index) = i64::from_str(key.as_str()) {
272 #[allow(clippy::cast_sign_loss)]
273 unsafe {
274 zend_hash_index_find(self, index as zend_ulong).as_mut()
275 }
276 } else {
277 unsafe {
278 zend_hash_str_find(
279 self,
280 CString::new(key.as_str()).ok()?.as_ptr(),
281 key.len() as _,
282 )
283 .as_mut()
284 }
285 }
286 }
287 ArrayKey::Str(key) => {
288 if let Ok(index) = i64::from_str(key) {
289 #[allow(clippy::cast_sign_loss)]
290 unsafe {
291 zend_hash_index_find(self, index as zend_ulong).as_mut()
292 }
293 } else {
294 unsafe {
295 zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
296 .as_mut()
297 }
298 }
299 }
300 }
301 }
302
303 /// Attempts to retrieve a value from the hash table with an index.
304 ///
305 /// # Parameters
306 ///
307 /// * `key` - The key to search for in the hash table.
308 ///
309 /// # Returns
310 ///
311 /// * `Some(&Zval)` - A reference to the zval at the position in the hash
312 /// table.
313 /// * `None` - No value at the given position was found.
314 ///
315 /// # Example
316 ///
317 /// ```no_run
318 /// use ext_php_rs::types::ZendHashTable;
319 ///
320 /// let mut ht = ZendHashTable::new();
321 ///
322 /// ht.push(100);
323 /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
324 /// ```
325 #[must_use]
326 pub fn get_index(&self, key: i64) -> Option<&Zval> {
327 #[allow(clippy::cast_sign_loss)]
328 unsafe {
329 zend_hash_index_find(self, key as zend_ulong).as_ref()
330 }
331 }
332
333 /// Attempts to retrieve a value from the hash table with an index.
334 ///
335 /// # Parameters
336 ///
337 /// * `key` - The key to search for in the hash table.
338 ///
339 /// # Returns
340 ///
341 /// * `Some(&Zval)` - A reference to the zval at the position in the hash
342 /// table.
343 /// * `None` - No value at the given position was found.
344 ///
345 /// # Example
346 ///
347 /// ```no_run
348 /// use ext_php_rs::types::ZendHashTable;
349 ///
350 /// let mut ht = ZendHashTable::new();
351 ///
352 /// ht.push(100);
353 /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
354 /// ```
355 // TODO: Verify if this is safe to use, as it allows mutating the
356 // hashtable while only having a reference to it. #461
357 #[allow(clippy::mut_from_ref)]
358 #[must_use]
359 pub fn get_index_mut(&self, key: i64) -> Option<&mut Zval> {
360 unsafe {
361 #[allow(clippy::cast_sign_loss)]
362 zend_hash_index_find(self, key as zend_ulong).as_mut()
363 }
364 }
365
366 /// Attempts to remove a value from the hash table with a string key.
367 ///
368 /// # Parameters
369 ///
370 /// * `key` - The key to remove from the hash table.
371 ///
372 /// # Returns
373 ///
374 /// * `Some(())` - Key was successfully removed.
375 /// * `None` - No key was removed, did not exist.
376 ///
377 /// # Example
378 ///
379 /// ```no_run
380 /// use ext_php_rs::types::ZendHashTable;
381 ///
382 /// let mut ht = ZendHashTable::new();
383 ///
384 /// ht.insert("test", "hello world");
385 /// assert_eq!(ht.len(), 1);
386 ///
387 /// ht.remove("test");
388 /// assert_eq!(ht.len(), 0);
389 /// ```
390 pub fn remove<'a, K>(&mut self, key: K) -> Option<()>
391 where
392 K: Into<ArrayKey<'a>>,
393 {
394 let result = match key.into() {
395 ArrayKey::Long(index) => unsafe {
396 #[allow(clippy::cast_sign_loss)]
397 zend_hash_index_del(self, index as zend_ulong)
398 },
399 ArrayKey::String(key) => {
400 if let Ok(index) = i64::from_str(key.as_str()) {
401 #[allow(clippy::cast_sign_loss)]
402 unsafe {
403 zend_hash_index_del(self, index as zend_ulong)
404 }
405 } else {
406 unsafe {
407 zend_hash_str_del(
408 self,
409 CString::new(key.as_str()).ok()?.as_ptr(),
410 key.len() as _,
411 )
412 }
413 }
414 }
415 ArrayKey::Str(key) => {
416 if let Ok(index) = i64::from_str(key) {
417 #[allow(clippy::cast_sign_loss)]
418 unsafe {
419 zend_hash_index_del(self, index as zend_ulong)
420 }
421 } else {
422 unsafe {
423 zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
424 }
425 }
426 }
427 };
428
429 if result < 0 {
430 None
431 } else {
432 Some(())
433 }
434 }
435
436 /// Attempts to remove a value from the hash table with a string key.
437 ///
438 /// # Parameters
439 ///
440 /// * `key` - The key to remove from the hash table.
441 ///
442 /// # Returns
443 ///
444 /// * `Ok(())` - Key was successfully removed.
445 /// * `None` - No key was removed, did not exist.
446 ///
447 /// # Example
448 ///
449 /// ```no_run
450 /// use ext_php_rs::types::ZendHashTable;
451 ///
452 /// let mut ht = ZendHashTable::new();
453 ///
454 /// ht.push("hello");
455 /// assert_eq!(ht.len(), 1);
456 ///
457 /// ht.remove_index(0);
458 /// assert_eq!(ht.len(), 0);
459 /// ```
460 pub fn remove_index(&mut self, key: i64) -> Option<()> {
461 let result = unsafe {
462 #[allow(clippy::cast_sign_loss)]
463 zend_hash_index_del(self, key as zend_ulong)
464 };
465
466 if result < 0 {
467 None
468 } else {
469 Some(())
470 }
471 }
472
473 /// Attempts to insert an item into the hash table, or update if the key
474 /// already exists. Returns nothing in a result if successful.
475 ///
476 /// # Parameters
477 ///
478 /// * `key` - The key to insert the value at in the hash table.
479 /// * `value` - 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 the key could not be converted into a [`CString`],
488 /// or converting the value into a [`Zval`] failed.
489 ///
490 /// # Example
491 ///
492 /// ```no_run
493 /// use ext_php_rs::types::ZendHashTable;
494 ///
495 /// let mut ht = ZendHashTable::new();
496 ///
497 /// ht.insert("a", "A");
498 /// ht.insert("b", "B");
499 /// ht.insert("c", "C");
500 /// assert_eq!(ht.len(), 3);
501 /// ```
502 pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()>
503 where
504 K: Into<ArrayKey<'a>>,
505 V: IntoZval,
506 {
507 let mut val = val.into_zval(false)?;
508 match key.into() {
509 ArrayKey::Long(index) => {
510 unsafe {
511 #[allow(clippy::cast_sign_loss)]
512 zend_hash_index_update(self, index as zend_ulong, &raw mut val)
513 };
514 }
515 ArrayKey::String(key) => {
516 if let Ok(index) = i64::from_str(&key) {
517 unsafe {
518 #[allow(clippy::cast_sign_loss)]
519 zend_hash_index_update(self, index as zend_ulong, &raw mut val)
520 };
521 } else {
522 unsafe {
523 zend_hash_str_update(
524 self,
525 CString::new(key.as_str())?.as_ptr(),
526 key.len(),
527 &raw mut val,
528 )
529 };
530 }
531 }
532 ArrayKey::Str(key) => {
533 if let Ok(index) = i64::from_str(key) {
534 unsafe {
535 #[allow(clippy::cast_sign_loss)]
536 zend_hash_index_update(self, index as zend_ulong, &raw mut val)
537 };
538 } else {
539 unsafe {
540 zend_hash_str_update(
541 self,
542 CString::new(key)?.as_ptr(),
543 key.len(),
544 &raw mut val,
545 )
546 };
547 }
548 }
549 }
550 val.release();
551 Ok(())
552 }
553
554 /// Inserts an item into the hash table at a specified index, or updates if
555 /// the key already exists. Returns nothing in a result if successful.
556 ///
557 /// # Parameters
558 ///
559 /// * `key` - The index at which the value should be inserted.
560 /// * `val` - The value to insert into the hash table.
561 ///
562 /// # Returns
563 ///
564 /// Returns nothing in a result on success.
565 ///
566 /// # Errors
567 ///
568 /// Returns an error if converting the value into a [`Zval`] failed.
569 ///
570 /// # Example
571 ///
572 /// ```no_run
573 /// use ext_php_rs::types::ZendHashTable;
574 ///
575 /// let mut ht = ZendHashTable::new();
576 ///
577 /// ht.insert_at_index(0, "A");
578 /// ht.insert_at_index(5, "B");
579 /// ht.insert_at_index(0, "C"); // notice overriding index 0
580 /// assert_eq!(ht.len(), 2);
581 /// ```
582 pub fn insert_at_index<V>(&mut self, key: i64, val: V) -> Result<()>
583 where
584 V: IntoZval,
585 {
586 let mut val = val.into_zval(false)?;
587 unsafe {
588 #[allow(clippy::cast_sign_loss)]
589 zend_hash_index_update(self, key as zend_ulong, &raw mut val)
590 };
591 val.release();
592 Ok(())
593 }
594
595 /// Pushes an item onto the end of the hash table. Returns a result
596 /// containing nothing if the element was successfully inserted.
597 ///
598 /// # Parameters
599 ///
600 /// * `val` - The value to insert into the hash table.
601 ///
602 /// # Returns
603 ///
604 /// Returns nothing in a result on success.
605 ///
606 /// # Errors
607 ///
608 /// Returns an error if converting the value into a [`Zval`] failed.
609 ///
610 /// # Example
611 ///
612 /// ```no_run
613 /// use ext_php_rs::types::ZendHashTable;
614 ///
615 /// let mut ht = ZendHashTable::new();
616 ///
617 /// ht.push("a");
618 /// ht.push("b");
619 /// ht.push("c");
620 /// assert_eq!(ht.len(), 3);
621 /// ```
622 pub fn push<V>(&mut self, val: V) -> Result<()>
623 where
624 V: IntoZval,
625 {
626 let mut val = val.into_zval(false)?;
627 unsafe { zend_hash_next_index_insert(self, &raw mut val) };
628 val.release();
629
630 Ok(())
631 }
632
633 /// Checks if the hashtable only contains numerical keys.
634 ///
635 /// # Returns
636 ///
637 /// True if all keys on the hashtable are numerical.
638 ///
639 /// # Example
640 ///
641 /// ```no_run
642 /// use ext_php_rs::types::ZendHashTable;
643 ///
644 /// let mut ht = ZendHashTable::new();
645 ///
646 /// ht.push(0);
647 /// ht.push(3);
648 /// ht.push(9);
649 /// assert!(ht.has_numerical_keys());
650 ///
651 /// ht.insert("obviously not numerical", 10);
652 /// assert!(!ht.has_numerical_keys());
653 /// ```
654 #[must_use]
655 pub fn has_numerical_keys(&self) -> bool {
656 !self.into_iter().any(|(k, _)| !k.is_long())
657 }
658
659 /// Checks if the hashtable has numerical, sequential keys.
660 ///
661 /// # Returns
662 ///
663 /// True if all keys on the hashtable are numerical and are in sequential
664 /// order (i.e. starting at 0 and not skipping any keys).
665 ///
666 /// # Panics
667 ///
668 /// Panics if the number of elements in the hashtable exceeds `i64::MAX`.
669 ///
670 /// # Example
671 ///
672 /// ```no_run
673 /// use ext_php_rs::types::ZendHashTable;
674 ///
675 /// let mut ht = ZendHashTable::new();
676 ///
677 /// ht.push(0);
678 /// ht.push(3);
679 /// ht.push(9);
680 /// assert!(ht.has_sequential_keys());
681 ///
682 /// ht.insert_at_index(90, 10);
683 /// assert!(!ht.has_sequential_keys());
684 /// ```
685 #[must_use]
686 pub fn has_sequential_keys(&self) -> bool {
687 !self
688 .into_iter()
689 .enumerate()
690 .any(|(i, (k, _))| ArrayKey::Long(i64::try_from(i).expect("Integer overflow")) != k)
691 }
692
693 /// Returns an iterator over the values contained inside the hashtable, as
694 /// if it was a set or list.
695 ///
696 /// # Example
697 ///
698 /// ```no_run
699 /// use ext_php_rs::types::ZendHashTable;
700 ///
701 /// let mut ht = ZendHashTable::new();
702 ///
703 /// for val in ht.values() {
704 /// dbg!(val);
705 /// }
706 #[inline]
707 #[must_use]
708 pub fn values(&self) -> Values<'_> {
709 Values::new(self)
710 }
711
712 /// Returns an iterator over the key(s) and value contained inside the
713 /// hashtable.
714 ///
715 /// # Example
716 ///
717 /// ```no_run
718 /// use ext_php_rs::types::{ZendHashTable, ArrayKey};
719 ///
720 /// let mut ht = ZendHashTable::new();
721 ///
722 /// for (key, val) in ht.iter() {
723 /// match &key {
724 /// ArrayKey::Long(index) => {
725 /// }
726 /// ArrayKey::String(key) => {
727 /// }
728 /// ArrayKey::Str(key) => {
729 /// }
730 /// }
731 /// dbg!(key, val);
732 /// }
733 #[inline]
734 #[must_use]
735 pub fn iter(&self) -> Iter<'_> {
736 self.into_iter()
737 }
738}
739
740unsafe impl ZBoxable for ZendHashTable {
741 fn free(&mut self) {
742 // SAFETY: ZBox has immutable access to `self`.
743 unsafe { zend_array_destroy(self) }
744 }
745}
746
747impl Debug for ZendHashTable {
748 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
749 f.debug_map()
750 .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
751 .finish()
752 }
753}
754
755impl ToOwned for ZendHashTable {
756 type Owned = ZBox<ZendHashTable>;
757
758 fn to_owned(&self) -> Self::Owned {
759 unsafe {
760 // SAFETY: FFI call does not modify `self`, returns a new hashtable.
761 let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
762
763 // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
764 ZBox::from_raw(
765 ptr.as_mut()
766 .expect("Failed to allocate memory for hashtable"),
767 )
768 }
769 }
770}
771
772/// Immutable iterator upon a reference to a hashtable.
773pub struct Iter<'a> {
774 ht: &'a ZendHashTable,
775 current_num: i64,
776 end_num: i64,
777 pos: HashPosition,
778 end_pos: HashPosition,
779}
780
781/// Represents the key of a PHP array, which can be either a long or a string.
782#[derive(Debug, Clone, PartialEq)]
783pub enum ArrayKey<'a> {
784 /// A numerical key.
785 /// In Zend API it's represented by `u64` (`zend_ulong`), so the value needs
786 /// to be cast to `zend_ulong` before passing into Zend functions.
787 Long(i64),
788 /// A string key.
789 String(String),
790 /// A string key by reference.
791 Str(&'a str),
792}
793
794impl From<String> for ArrayKey<'_> {
795 fn from(value: String) -> Self {
796 Self::String(value)
797 }
798}
799
800impl ArrayKey<'_> {
801 /// Check if the key is an integer.
802 ///
803 /// # Returns
804 ///
805 /// Returns true if the key is an integer, false otherwise.
806 #[must_use]
807 pub fn is_long(&self) -> bool {
808 match self {
809 ArrayKey::Long(_) => true,
810 ArrayKey::String(_) | ArrayKey::Str(_) => false,
811 }
812 }
813}
814
815impl Display for ArrayKey<'_> {
816 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
817 match self {
818 ArrayKey::Long(key) => write!(f, "{key}"),
819 ArrayKey::String(key) => write!(f, "{key}"),
820 ArrayKey::Str(key) => write!(f, "{key}"),
821 }
822 }
823}
824
825impl<'a> From<&'a str> for ArrayKey<'a> {
826 fn from(key: &'a str) -> ArrayKey<'a> {
827 ArrayKey::Str(key)
828 }
829}
830
831impl<'a> From<i64> for ArrayKey<'a> {
832 fn from(index: i64) -> ArrayKey<'a> {
833 ArrayKey::Long(index)
834 }
835}
836
837impl<'a> FromZval<'a> for ArrayKey<'_> {
838 const TYPE: DataType = DataType::String;
839
840 fn from_zval(zval: &'a Zval) -> Option<Self> {
841 if let Some(key) = zval.long() {
842 return Some(ArrayKey::Long(key));
843 }
844 if let Some(key) = zval.string() {
845 return Some(ArrayKey::String(key));
846 }
847 None
848 }
849}
850
851impl<'a> Iter<'a> {
852 /// Creates a new iterator over a hashtable.
853 ///
854 /// # Parameters
855 ///
856 /// * `ht` - The hashtable to iterate.
857 pub fn new(ht: &'a ZendHashTable) -> Self {
858 let end_num: i64 = ht
859 .len()
860 .try_into()
861 .expect("Integer overflow in hashtable length");
862 let end_pos = if ht.nNumOfElements > 0 {
863 ht.nNumOfElements - 1
864 } else {
865 0
866 };
867
868 Self {
869 ht,
870 current_num: 0,
871 end_num,
872 pos: 0,
873 end_pos,
874 }
875 }
876}
877
878impl<'a> IntoIterator for &'a ZendHashTable {
879 type Item = (ArrayKey<'a>, &'a Zval);
880 type IntoIter = Iter<'a>;
881
882 /// Returns an iterator over the key(s) and value contained inside the
883 /// hashtable.
884 ///
885 /// # Example
886 ///
887 /// ```no_run
888 /// use ext_php_rs::types::ZendHashTable;
889 ///
890 /// let mut ht = ZendHashTable::new();
891 ///
892 /// for (key, val) in ht.iter() {
893 /// // ^ Index if inserted at an index.
894 /// // ^ Optional string key, if inserted like a hashtable.
895 /// // ^ Inserted value.
896 ///
897 /// dbg!(key, val);
898 /// }
899 #[inline]
900 fn into_iter(self) -> Self::IntoIter {
901 Iter::new(self)
902 }
903}
904
905impl<'a> Iterator for Iter<'a> {
906 type Item = (ArrayKey<'a>, &'a Zval);
907
908 fn next(&mut self) -> Option<Self::Item> {
909 self.next_zval()
910 .map(|(k, v)| (ArrayKey::from_zval(&k).expect("Invalid array key!"), v))
911 }
912
913 fn count(self) -> usize
914 where
915 Self: Sized,
916 {
917 self.ht.len()
918 }
919}
920
921impl ExactSizeIterator for Iter<'_> {
922 fn len(&self) -> usize {
923 self.ht.len()
924 }
925}
926
927impl DoubleEndedIterator for Iter<'_> {
928 fn next_back(&mut self) -> Option<Self::Item> {
929 if self.end_num <= self.current_num {
930 return None;
931 }
932
933 let key_type = unsafe {
934 zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos)
935 };
936
937 if key_type == -1 {
938 return None;
939 }
940
941 let key = Zval::new();
942
943 unsafe {
944 zend_hash_get_current_key_zval_ex(
945 ptr::from_ref(self.ht).cast_mut(),
946 (&raw const key).cast_mut(),
947 &raw mut self.end_pos,
948 );
949 }
950 let value = unsafe {
951 &*zend_hash_get_current_data_ex(
952 ptr::from_ref(self.ht).cast_mut(),
953 &raw mut self.end_pos,
954 )
955 };
956
957 let key = match ArrayKey::from_zval(&key) {
958 Some(key) => key,
959 None => ArrayKey::Long(self.end_num),
960 };
961
962 unsafe {
963 zend_hash_move_backwards_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.end_pos)
964 };
965 self.end_num -= 1;
966
967 Some((key, value))
968 }
969}
970
971impl<'a> Iter<'a> {
972 pub fn next_zval(&mut self) -> Option<(Zval, &'a Zval)> {
973 if self.current_num >= self.end_num {
974 return None;
975 }
976
977 let key_type = unsafe {
978 zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos)
979 };
980
981 // Key type `-1` is ???
982 // Key type `1` is string
983 // Key type `2` is long
984 // Key type `3` is null meaning the end of the array
985 if key_type == -1 || key_type == 3 {
986 return None;
987 }
988
989 let mut key = Zval::new();
990
991 unsafe {
992 zend_hash_get_current_key_zval_ex(
993 ptr::from_ref(self.ht).cast_mut(),
994 (&raw const key).cast_mut(),
995 &raw mut self.pos,
996 );
997 }
998 let value = unsafe {
999 let val_ptr =
1000 zend_hash_get_current_data_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos);
1001
1002 if val_ptr.is_null() {
1003 return None;
1004 }
1005
1006 &*val_ptr
1007 };
1008
1009 if !key.is_long() && !key.is_string() {
1010 key.set_long(self.current_num);
1011 }
1012
1013 unsafe { zend_hash_move_forward_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos) };
1014 self.current_num += 1;
1015
1016 Some((key, value))
1017 }
1018}
1019
1020/// Immutable iterator which iterates over the values of the hashtable, as it
1021/// was a set or list.
1022pub struct Values<'a>(Iter<'a>);
1023
1024impl<'a> Values<'a> {
1025 /// Creates a new iterator over a hashtables values.
1026 ///
1027 /// # Parameters
1028 ///
1029 /// * `ht` - The hashtable to iterate.
1030 pub fn new(ht: &'a ZendHashTable) -> Self {
1031 Self(Iter::new(ht))
1032 }
1033}
1034
1035impl<'a> Iterator for Values<'a> {
1036 type Item = &'a Zval;
1037
1038 fn next(&mut self) -> Option<Self::Item> {
1039 self.0.next().map(|(_, zval)| zval)
1040 }
1041
1042 fn count(self) -> usize
1043 where
1044 Self: Sized,
1045 {
1046 self.0.count()
1047 }
1048}
1049
1050impl ExactSizeIterator for Values<'_> {
1051 fn len(&self) -> usize {
1052 self.0.len()
1053 }
1054}
1055
1056impl DoubleEndedIterator for Values<'_> {
1057 fn next_back(&mut self) -> Option<Self::Item> {
1058 self.0.next_back().map(|(_, zval)| zval)
1059 }
1060}
1061
1062impl Default for ZBox<ZendHashTable> {
1063 fn default() -> Self {
1064 ZendHashTable::new()
1065 }
1066}
1067
1068impl Clone for ZBox<ZendHashTable> {
1069 fn clone(&self) -> Self {
1070 (**self).to_owned()
1071 }
1072}
1073
1074impl IntoZval for ZBox<ZendHashTable> {
1075 const TYPE: DataType = DataType::Array;
1076 const NULLABLE: bool = false;
1077
1078 fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1079 zv.set_hashtable(self);
1080 Ok(())
1081 }
1082}
1083
1084impl<'a> FromZval<'a> for &'a ZendHashTable {
1085 const TYPE: DataType = DataType::Array;
1086
1087 fn from_zval(zval: &'a Zval) -> Option<Self> {
1088 zval.array()
1089 }
1090}
1091
1092///////////////////////////////////////////
1093// HashMap
1094///////////////////////////////////////////
1095
1096// TODO: Generalize hasher
1097#[allow(clippy::implicit_hasher)]
1098impl<'a, V> TryFrom<&'a ZendHashTable> for HashMap<String, V>
1099where
1100 V: FromZval<'a>,
1101{
1102 type Error = Error;
1103
1104 fn try_from(value: &'a ZendHashTable) -> Result<Self> {
1105 let mut hm = HashMap::with_capacity(value.len());
1106
1107 for (key, val) in value {
1108 hm.insert(
1109 key.to_string(),
1110 V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?,
1111 );
1112 }
1113
1114 Ok(hm)
1115 }
1116}
1117
1118impl<K, V> TryFrom<HashMap<K, V>> for ZBox<ZendHashTable>
1119where
1120 K: AsRef<str>,
1121 V: IntoZval,
1122{
1123 type Error = Error;
1124
1125 fn try_from(value: HashMap<K, V>) -> Result<Self> {
1126 let mut ht = ZendHashTable::with_capacity(
1127 value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
1128 );
1129
1130 for (k, v) in value {
1131 ht.insert(k.as_ref(), v)?;
1132 }
1133
1134 Ok(ht)
1135 }
1136}
1137
1138// TODO: Generalize hasher
1139#[allow(clippy::implicit_hasher)]
1140impl<K, V> IntoZval for HashMap<K, V>
1141where
1142 K: AsRef<str>,
1143 V: IntoZval,
1144{
1145 const TYPE: DataType = DataType::Array;
1146 const NULLABLE: bool = false;
1147
1148 fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1149 let arr = self.try_into()?;
1150 zv.set_hashtable(arr);
1151 Ok(())
1152 }
1153}
1154
1155// TODO: Generalize hasher
1156#[allow(clippy::implicit_hasher)]
1157impl<'a, T> FromZval<'a> for HashMap<String, T>
1158where
1159 T: FromZval<'a>,
1160{
1161 const TYPE: DataType = DataType::Array;
1162
1163 fn from_zval(zval: &'a Zval) -> Option<Self> {
1164 zval.array().and_then(|arr| arr.try_into().ok())
1165 }
1166}
1167
1168///////////////////////////////////////////
1169// Vec
1170///////////////////////////////////////////
1171
1172impl<'a, T> TryFrom<&'a ZendHashTable> for Vec<T>
1173where
1174 T: FromZval<'a>,
1175{
1176 type Error = Error;
1177
1178 fn try_from(value: &'a ZendHashTable) -> Result<Self> {
1179 let mut vec = Vec::with_capacity(value.len());
1180
1181 for (_, val) in value {
1182 vec.push(T::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?);
1183 }
1184
1185 Ok(vec)
1186 }
1187}
1188
1189impl<T> TryFrom<Vec<T>> for ZBox<ZendHashTable>
1190where
1191 T: IntoZval,
1192{
1193 type Error = Error;
1194
1195 fn try_from(value: Vec<T>) -> Result<Self> {
1196 let mut ht = ZendHashTable::with_capacity(
1197 value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
1198 );
1199
1200 for val in value {
1201 ht.push(val)?;
1202 }
1203
1204 Ok(ht)
1205 }
1206}
1207
1208impl<T> IntoZval for Vec<T>
1209where
1210 T: IntoZval,
1211{
1212 const TYPE: DataType = DataType::Array;
1213 const NULLABLE: bool = false;
1214
1215 fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1216 let arr = self.try_into()?;
1217 zv.set_hashtable(arr);
1218 Ok(())
1219 }
1220}
1221
1222impl<'a, T> FromZval<'a> for Vec<T>
1223where
1224 T: FromZval<'a>,
1225{
1226 const TYPE: DataType = DataType::Array;
1227
1228 fn from_zval(zval: &'a Zval) -> Option<Self> {
1229 zval.array().and_then(|arr| arr.try_into().ok())
1230 }
1231}
1232
1233impl FromIterator<Zval> for ZBox<ZendHashTable> {
1234 fn from_iter<T: IntoIterator<Item = Zval>>(iter: T) -> Self {
1235 let mut ht = ZendHashTable::new();
1236 for item in iter {
1237 // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1238 // `val` to a zval fails.
1239 let _ = ht.push(item);
1240 }
1241 ht
1242 }
1243}
1244
1245impl FromIterator<(i64, Zval)> for ZBox<ZendHashTable> {
1246 fn from_iter<T: IntoIterator<Item = (i64, Zval)>>(iter: T) -> Self {
1247 let mut ht = ZendHashTable::new();
1248 for (key, val) in iter {
1249 // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1250 // `val` to a zval fails.
1251 let _ = ht.insert_at_index(key, val);
1252 }
1253 ht
1254 }
1255}
1256
1257impl<'a> FromIterator<(&'a str, Zval)> for ZBox<ZendHashTable> {
1258 fn from_iter<T: IntoIterator<Item = (&'a str, Zval)>>(iter: T) -> Self {
1259 let mut ht = ZendHashTable::new();
1260 for (key, val) in iter {
1261 // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1262 // `val` to a zval fails.
1263 let _ = ht.insert(key, val);
1264 }
1265 ht
1266 }
1267}