From cc351a2f05a88221cf8fbedf0cbe726dfc79a8e5 Mon Sep 17 00:00:00 2001
From: flashwave <me@flash.moe>
Date: Sat, 15 Mar 2025 00:00:53 +0000
Subject: [PATCH] Made FormContent iterable.

---
 VERSION                                    |  2 +-
 src/Http/Content/FormContent.php           |  9 +++--
 src/Http/Content/MultipartFormContent.php  | 38 ++++++++++++++++++++--
 src/Http/Content/UrlEncodedFormContent.php | 38 ++++++++++++++++++++--
 tests/HttpFormContentTest.php              | 28 ++++++++++++++++
 5 files changed, 108 insertions(+), 7 deletions(-)

diff --git a/VERSION b/VERSION
index bdd4fdb..99f6ee8 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.2503.142326
+0.2503.142359
diff --git a/src/Http/Content/FormContent.php b/src/Http/Content/FormContent.php
index 787ef11..32fb08c 100644
--- a/src/Http/Content/FormContent.php
+++ b/src/Http/Content/FormContent.php
@@ -1,14 +1,19 @@
 <?php
 // FormContent.php
 // Created: 2025-03-12
-// Updated: 2025-03-12
+// Updated: 2025-03-14
 
 namespace Index\Http\Content;
 
+use Iterator;
+
 /**
  * Provides a common interface for application/x-www-form-urlencoded and multipart/form-data.
+ *
+ * @template TValue of string|\Stringable
+ * @extends Iterator<string, ?list<TValue>>
  */
-interface FormContent extends Content {
+interface FormContent extends Content, Iterator {
     /**
      * Checks if a form field is present.
      *
diff --git a/src/Http/Content/MultipartFormContent.php b/src/Http/Content/MultipartFormContent.php
index 6b43a7c..8888373 100644
--- a/src/Http/Content/MultipartFormContent.php
+++ b/src/Http/Content/MultipartFormContent.php
@@ -1,7 +1,7 @@
 <?php
 // MultipartFormContent.php
 // Created: 2025-03-12
-// Updated: 2025-03-12
+// Updated: 2025-03-14
 
 namespace Index\Http\Content;
 
@@ -12,8 +12,16 @@ use Psr\Http\Message\StreamInterface;
 
 /**
  * Implements multipart/form-data.
+ *
+ * @implements FormContent<MultipartFormData>
  */
 class MultipartFormContent implements FormContent {
+    /** @var string[] */
+    private array $keys;
+
+    /** @var int<0, max> */
+    private int $position = 0;
+
     /**
      * @param StreamInterface $stream Raw request body stream.
      * @param array<string, list<MultipartFormData>> $params Form parameters.
@@ -23,7 +31,33 @@ class MultipartFormContent implements FormContent {
         public private(set) StreamInterface $stream,
         public private(set) array $params,
         public private(set) array $files,
-    ) {}
+    ) {
+        $this->keys = array_keys($params);
+    }
+
+    public function rewind(): void {
+        $this->position = 0;
+    }
+
+    public function valid(): bool {
+        return $this->position < count($this->keys);
+    }
+
+    /** @return ?list<?MultipartFormData> */
+    #[\ReturnTypeWillChange]
+    public function current() {
+        return $this->valid() ? $this->params[$this->keys[$this->position]] : null;
+    }
+
+    /** @return ?string */
+    #[\ReturnTypeWillChange]
+    public function key() {
+        return $this->valid() ? $this->keys[$this->position] : null;
+    }
+
+    public function next(): void {
+        ++$this->position;
+    }
 
     public function hasParam(string $name): bool {
         return isset($this->params[$name]);
diff --git a/src/Http/Content/UrlEncodedFormContent.php b/src/Http/Content/UrlEncodedFormContent.php
index b6e841e..0bf2525 100644
--- a/src/Http/Content/UrlEncodedFormContent.php
+++ b/src/Http/Content/UrlEncodedFormContent.php
@@ -1,7 +1,7 @@
 <?php
 // UrlEncodedFormContent.php
 // Created: 2025-03-12
-// Updated: 2025-03-12
+// Updated: 2025-03-14
 
 namespace Index\Http\Content;
 
@@ -10,8 +10,16 @@ use Psr\Http\Message\StreamInterface;
 
 /**
  * Implements application/x-www-form-urlencoded.
+ *
+ * @implements FormContent<string>
  */
 class UrlEncodedFormContent implements FormContent {
+    /** @var string[] */
+    private array $keys;
+
+    /** @var int<0, max> */
+    private int $position = 0;
+
     /**
      * @param StreamInterface $stream Raw request body stream.
      * @param array<string, list<?string>> $params Form parameters.
@@ -19,7 +27,33 @@ class UrlEncodedFormContent implements FormContent {
     public function __construct(
         public private(set) StreamInterface $stream,
         public private(set) array $params,
-    ) {}
+    ) {
+        $this->keys = array_keys($params);
+    }
+
+    public function rewind(): void {
+        $this->position = 0;
+    }
+
+    public function valid(): bool {
+        return $this->position < count($this->keys);
+    }
+
+    /** @return ?list<?string> */
+    #[\ReturnTypeWillChange]
+    public function current() {
+        return $this->valid() ? $this->params[$this->keys[$this->position]] : null;
+    }
+
+    /** @return ?string */
+    #[\ReturnTypeWillChange]
+    public function key() {
+        return $this->valid() ? $this->keys[$this->position] : null;
+    }
+
+    public function next(): void {
+        ++$this->position;
+    }
 
     public function hasParam(string $name): bool {
         return isset($this->params[$name]);
diff --git a/tests/HttpFormContentTest.php b/tests/HttpFormContentTest.php
index 808b9ce..870b004 100644
--- a/tests/HttpFormContentTest.php
+++ b/tests/HttpFormContentTest.php
@@ -75,6 +75,9 @@ final class HttpFormContentTest extends TestCase {
             for($i = 0; $i < $count; ++$i)
                 $this->assertEquals($form->getParamAt($key, $i), $values[$i]);
         }
+
+        $extracted = iterator_to_array($form);
+        $this->assertEquals($expected, $extracted);
     }
 
     public function testMultipartForm(): void {
@@ -136,5 +139,30 @@ final class HttpFormContentTest extends TestCase {
         $this->assertIsString($the2Path);
         $the2->moveTo($the2Path);
         $this->assertEquals($the2Hash, hash_file('sha256', $the2Path));
+
+        $expected = [
+            'meow' => ['sfdsfs'],
+            'mewow' => ['https://railgun.sh/sockchat'],
+            '"the' => [
+                'value!',
+                base64_decode(
+                    'iVBORw0KGgoAAAANSUhEUgAAABkAAAAdCAYAAABfeMd1AAAABHNCSVQICAgIfAhkiAAAArVJREFUSEu1'
+                  . 'Vj9o00EUfr8gdNFBKtVEsUvjUgfpFBQKKrR1tRlUEEwFB8U4OHXroHSqaAanSgRRI0ZRpCUGFAwKpUJF'
+                  . 'cEqqECiJLRgHC6XT2e/0Xe/3ctekBg9+9N37833vvb67C1GLFd9JqoVLZ2YQrNQXFf7+NzIAT6bTaqFU'
+                  . '6ogs8NVqZz42lqaL42njeize5wyrrJITz6kEwYfKohOIlUuVmhZf5/M0pfJa3p39o5NkTSQgeDJbMgQH'
+                  . '4jEjS8FFtD6kaN/ZOtlEIRIQoDVy2a2StnaInJW0apWPaOjpGW2S1UTsgHb+F5IAe27p9SCpzV3FgL4/'
+                  . 'jhIPjyHxEaAd3BIXga0bTiaJiWx9qBLbwOCYHs703mTGuEC2yV0DwtVEUIF9JlyZuypB1kjAZev7eFC3'
+                  . 'jJeuZO7BafpcukF2ptADhBeDcRLInImMkyWAaHn/FY0dwTwnzj+nteoCvZyfN8AguHR8icYzGRoYHNSf'
+                  . 'JIYO9p5o8w3QaDQ0JrBNTWhZ9+EEwZgaGaFsoUDlcpmUUnRib1YTzBRqdOf+ism3mDui5bfLKQqCgDbu'
+                  . 'OR3Hqz71Sx/MHazQJ/TLnApih7Rjbnqaen/cNgEsXLvQY3TF3KZZEjSGV40xNF0gUrUyvX832wTeSoHW'
+                  . 'Ig7JoRP2MpX4QNY+3dxok9uK9mE1Xoy6Hf5qtyT59nWG+nsHDMCb0bshsJPPLnvBcbXwajqMaBneCx5f'
+                  . 'TAg+37Jt1Yf9oavfS2KDnZtYJ1SDD2BHbyWMGbJNAJ9T1W5tt6vAvukWZhSMNKri2+DRRJfNH5J94HDC'
+                  . 'CHtJJKJ99Ujbz9TmwyarCJ0TGSj38km17fFsTdlEMrbtSmSg3KNSJuJq5DMsY/5pD6I9V2Nq16to6Gbf'
+                  . '8pxsl0kPClpH/h8f28X0+ss3yuvYiUFO4m81NF/DgbLLlAAAAABJRU5ErkJggg=='
+                ),
+            ],
+        ];
+        $extracted = iterator_to_array($form);
+        $this->assertEquals($expected, $extracted);
     }
 }