From a6355abb3b1bbe2898c08a81f7642c810be39953 Mon Sep 17 00:00:00 2001
From: Juan Treminio <jtreminio@gmail.com>
Date: Fri, 28 Mar 2025 17:40:41 -0500
Subject: [PATCH 01/10] [PHP] - Add FormDataProcessor to handle nested
 ModelInterface data

---
 .../codegen/languages/PhpClientCodegen.java   |   1 +
 .../resources/php/FormDataProcessor.mustache  | 234 ++++++++++++++++++
 .../resources/php/ObjectSerializer.mustache   | 107 --------
 .../src/main/resources/php/api.mustache       |  29 +--
 4 files changed, 248 insertions(+), 123 deletions(-)
 create mode 100644 modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache

diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpClientCodegen.java
index 686c389846e6..baeae7e39486 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpClientCodegen.java
@@ -117,6 +117,7 @@ public void processOpts() {
 
         supportingFiles.add(new SupportingFile("ApiException.mustache", toSrcPath(invokerPackage, srcBasePath), "ApiException.php"));
         supportingFiles.add(new SupportingFile("Configuration.mustache", toSrcPath(invokerPackage, srcBasePath), "Configuration.php"));
+        supportingFiles.add(new SupportingFile("FormDataProcessor.mustache", toSrcPath(invokerPackage, srcBasePath), "FormDataProcessor.php"));
         supportingFiles.add(new SupportingFile("ObjectSerializer.mustache", toSrcPath(invokerPackage, srcBasePath), "ObjectSerializer.php"));
         supportingFiles.add(new SupportingFile("ModelInterface.mustache", toSrcPath(modelPackage, srcBasePath), "ModelInterface.php"));
         supportingFiles.add(new SupportingFile("HeaderSelector.mustache", toSrcPath(invokerPackage, srcBasePath), "HeaderSelector.php"));
diff --git a/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache b/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache
new file mode 100644
index 000000000000..3db913860835
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache
@@ -0,0 +1,234 @@
+<?php
+/**
+ * ObjectSerializer
+ *
+ * PHP version 7.4
+ *
+ * @category Class
+ * @package  {{invokerPackage}}
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+{{>partial_header}}
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace {{invokerPackage}};
+
+use ArrayAccess;
+use DateTime;
+use GuzzleHttp\Psr7\Utils;
+use Psr\Http\Message\StreamInterface;
+use SplFileObject;
+use {{modelPackage}}\ModelInterface;
+
+/**
+ * FormDataProcessor Class Doc Comment
+ *
+ * @category Class
+ * @package  {{invokerPackage}}
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+class FormDataProcessor
+{
+    /**
+     * Tags whether payload passed to ::prepare() contains one or more
+     * SplFileObject or stream values.
+     */
+    public bool $has_file = false;
+
+    /**
+     * Take value and turn it into an array suitable for inclusion in
+     * the http body (form parameter). If it's a string, pass through unchanged
+     * If it's a datetime object, format it in ISO8601
+     *
+     * @param string|bool|array|DateTime|ArrayAccess|SplFileObject $values the value of the form parameter
+     *
+     * @return array [key => value] of formdata
+     */
+    public function prepare(array $values): array
+    {
+        $this->has_file = false;
+        $result = [];
+
+        foreach ($values as $k => $v) {
+            if ($v === null) {
+                continue;
+            }
+
+            $result[$k] = $this->makeFormSafe($v);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Flattens a multi-level array of data and generates a single-level array
+     * compatible with formdata - a single-level array where the keys use bracket
+     * notation to signify nested data.
+     *
+     * credit: https://github.com/FranBar1966/FlatPHP
+     */
+    public static function flatten(array $source, string $start = ''): array
+    {
+        $opt = [
+            'prefix'          => '[',
+            'suffix'          => ']',
+            'suffix-end'      => true,
+            'prefix-list'     => '[',
+            'suffix-list'     => ']',
+            'suffix-list-end' => true,
+        ];
+
+        if ($start === '') {
+            $currentPrefix    = '';
+            $currentSuffix    = '';
+            $currentSuffixEnd = false;
+        } elseif (array_is_list($source)) {
+            $currentPrefix    = $opt['prefix-list'];
+            $currentSuffix    = $opt['suffix-list'];
+            $currentSuffixEnd = $opt['suffix-list-end'];
+        } else {
+            $currentPrefix    = $opt['prefix'];
+            $currentSuffix    = $opt['suffix'];
+            $currentSuffixEnd = $opt['suffix-end'];
+        }
+
+        $currentName = $start;
+        $result = [];
+
+        foreach ($source as $key => $val) {
+            $currentName .= $currentPrefix.$key;
+
+            if (is_array($val) && !empty($val)) {
+                $currentName .= $currentSuffix;
+                $result += self::flatten($val, $currentName);
+            } else {
+                if ($currentSuffixEnd) {
+                    $currentName .= $currentSuffix;
+                }
+
+                $result[$currentName] = ObjectSerializer::toString($val);
+            }
+
+            $currentName = $start;
+        }
+
+        return $result;
+    }
+
+    /**
+     * formdata must be limited to scalars or arrays of scalar values,
+     * or a resource for a file upload. Here we iterate through all available
+     * data and identify how to handle each scenario
+     */
+    protected function makeFormSafe($value)
+    {
+        if ($value instanceof SplFileObject) {
+            return $this->processFiles([$value])[0];
+        }
+
+        if (is_resource($value)) {
+            $this->has_file = true;
+
+            return $value;
+        }
+
+        if ($value instanceof ModelInterface) {
+            return $this->processModel($value);
+        }
+
+        if (is_array($value) || is_object($value)) {
+            $data = [];
+
+            foreach ($value as $k => $v) {
+                $data[$k] = $this->makeFormSafe($v);
+            }
+
+            return $data;
+        }
+
+        return ObjectSerializer::toString($value);
+    }
+
+    /**
+     * We are able to handle nested ModelInterface. We do not simply call
+     * json_decode(json_encode()) because any given model may have binary data
+     * or other data that cannot be serialized to a JSON string
+     */
+    protected function processModel(ModelInterface $model): array
+    {
+        $result = [];
+
+        foreach ($model::openAPITypes() as $name => $type) {
+            $value = $model->offsetGet($name);
+
+            if ($value === null) {
+                continue;
+            }
+
+            if (strpos($type, '\SplFileObject') !== false) {
+                $file = is_array($value) ? $value : [$value];
+                $result[$name] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($value instanceof ModelInterface) {
+                $result[$name] = $this->processModel($value);
+
+                continue;
+            }
+
+            if (is_array($value) || is_object($value)) {
+                $result[$name] = $this->makeFormSafe($value);
+
+                continue;
+            }
+
+            $result[$name] = ObjectSerializer::toString($value);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Handle file data
+     */
+    protected function processFiles(array $files): array
+    {
+        $this->has_file = true;
+
+        $result = [];
+
+        foreach ($files as $i => $file) {
+            if (is_array($file)) {
+                $result[$i] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($file instanceof StreamInterface) {
+                $result[$i] = $file;
+
+                continue;
+            }
+
+            if ($file instanceof SplFileObject) {
+                $result[$i] = $this->tryFopen($file);
+            }
+        }
+
+        return $result;
+    }
+
+    private function tryFopen(SplFileObject $file)
+    {
+        return Utils::tryFopen($file->getRealPath(), 'rb');
+    }
+}
diff --git a/modules/openapi-generator/src/main/resources/php/ObjectSerializer.mustache b/modules/openapi-generator/src/main/resources/php/ObjectSerializer.mustache
index e25b25ce28d7..e3e0ed9d9bae 100644
--- a/modules/openapi-generator/src/main/resources/php/ObjectSerializer.mustache
+++ b/modules/openapi-generator/src/main/resources/php/ObjectSerializer.mustache
@@ -19,7 +19,6 @@
 
 namespace {{invokerPackage}};
 
-use ArrayAccess;
 use GuzzleHttp\Psr7\Utils;
 use {{modelPackage}}\ModelInterface;
 
@@ -315,35 +314,6 @@ class ObjectSerializer
         return self::toString($value);
     }
 
-    /**
-     * Take value and turn it into an array suitable for inclusion in
-     * the http body (form parameter). If it's a string, pass through unchanged
-     * If it's a datetime object, format it in ISO8601
-     *
-     * @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter
-     *
-     * @return array [key => value] of formdata
-     */
-    public static function toFormValue(string $key, mixed $value)
-    {
-        if ($value instanceof \SplFileObject) {
-            return [$key => $value->getRealPath()];
-        } elseif (is_array($value) || $value instanceof ArrayAccess) {
-            $flattened = [];
-            $result = [];
-
-            self::flattenArray(json_decode(json_encode($value), true), $flattened);
-
-            foreach ($flattened as $k => $v) {
-                $result["{$key}{$k}"] = self::toString($v);
-            }
-
-            return $result;
-        } else {
-            return [$key => self::toString($value)];
-        }
-    }
-
     /**
      * Take value and turn it into a string suitable for inclusion in
      * the parameter. If it's a string, pass through unchanged
@@ -617,81 +587,4 @@ class ObjectSerializer
 
         return $qs ? (string) substr($qs, 0, -1) : '';
     }
-
-    /**
-     * Flattens an array of Model object and generates an array compatible
-     * with formdata - a single-level array where the keys use bracket
-     * notation to signify nested data.
-     *
-     * @param \ArrayAccess|array $source
-     *
-     * credit: https://github.com/FranBar1966/FlatPHP
-     */
-    private static function flattenArray(
-        mixed $source,
-        array &$destination,
-        string $start = '',
-    ) {
-        $opt = [
-            'prefix'          => '[',
-            'suffix'          => ']',
-            'suffix-end'      => true,
-            'prefix-list'     => '[',
-            'suffix-list'     => ']',
-            'suffix-list-end' => true,
-        ];
-
-        if (!is_array($source)) {
-            $source = (array) $source;
-        }
-
-        /**
-         * array_is_list only in PHP >= 8.1
-         *
-         * credit: https://www.php.net/manual/en/function.array-is-list.php#127044
-         */
-        if (!function_exists('array_is_list')) {
-            function array_is_list(array $array)
-            {
-                $i = -1;
-
-                foreach ($array as $k => $v) {
-                    ++$i;
-                    if ($k !== $i) {
-                        return false;
-                    }
-                }
-
-                return true;
-            }
-        }
-
-        if (array_is_list($source)) {
-            $currentPrefix    = $opt['prefix-list'];
-            $currentSuffix    = $opt['suffix-list'];
-            $currentSuffixEnd = $opt['suffix-list-end'];
-        } else {
-            $currentPrefix    = $opt['prefix'];
-            $currentSuffix    = $opt['suffix'];
-            $currentSuffixEnd = $opt['suffix-end'];
-        }
-
-        $currentName = $start;
-
-        foreach ($source as $key => $val) {
-            $currentName .= $currentPrefix.$key;
-
-            if (is_array($val) && !empty($val)) {
-                $currentName .= "{$currentSuffix}";
-                self::flattenArray($val, $destination, $currentName);
-            } else {
-                if ($currentSuffixEnd) {
-                    $currentName .= $currentSuffix;
-                }
-                $destination[$currentName] = self::toString($val);
-            }
-
-            $currentName = $start;
-        }
-    }
 }
diff --git a/modules/openapi-generator/src/main/resources/php/api.mustache b/modules/openapi-generator/src/main/resources/php/api.mustache
index f6246863be4d..80615cdda9d5 100644
--- a/modules/openapi-generator/src/main/resources/php/api.mustache
+++ b/modules/openapi-generator/src/main/resources/php/api.mustache
@@ -27,6 +27,7 @@ use GuzzleHttp\Psr7\Request;
 use GuzzleHttp\RequestOptions;
 use {{invokerPackage}}\ApiException;
 use {{invokerPackage}}\Configuration;
+use {{invokerPackage}}\FormDataProcessor;
 use {{invokerPackage}}\HeaderSelector;
 use {{invokerPackage}}\ObjectSerializer;
 
@@ -669,23 +670,19 @@ use {{invokerPackage}}\ObjectSerializer;
         {{/pathParams}}
 
         {{#formParams}}
+        {{#-first}}
         // form params
-        if (${{paramName}} !== null) {
-            {{#isFile}}
-            $multipart = true;
-            $formParams['{{baseName}}'] = [];
-            $paramFiles = is_array(${{paramName}}) ? ${{paramName}} : [${{paramName}}];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['{{baseName}}'][] = \GuzzleHttp\Psr7\Utils::tryFopen(
-                    ObjectSerializer::toFormValue('{{baseName}}', $paramFile)['{{baseName}}'],
-                    'rb'
-                );
-            }
-            {{/isFile}}
-            {{^isFile}}
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('{{baseName}}', ${{paramName}}));
-            {{/isFile}}
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+        {{/-first}}
+            '{{paramName}}' => ${{paramName}},
+        {{#-last}}
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
+        {{/-last}}
         {{/formParams}}
 
         {{#isMultipart}}

From e9046a6a0ccce8cbbc069f1923066c0f10e28f60 Mon Sep 17 00:00:00 2001
From: Juan Treminio <jtreminio@gmail.com>
Date: Fri, 28 Mar 2025 17:42:23 -0500
Subject: [PATCH 02/10] Generating samples

---
 .../lib/Api/AnotherFakeApi.php                |   1 +
 .../OpenAPIClient-php/lib/Api/DefaultApi.php  |   1 +
 .../php/OpenAPIClient-php/lib/Api/FakeApi.php | 117 ++++++----------
 .../lib/Api/FakeClassnameTags123Api.php       |   1 +
 .../php/OpenAPIClient-php/lib/Api/PetApi.php  | 127 ++++++------------
 .../OpenAPIClient-php/lib/Api/StoreApi.php    |   1 +
 .../php/OpenAPIClient-php/lib/Api/UserApi.php |   1 +
 .../lib/ObjectSerializer.php                  | 107 ---------------
 .../php/psr-18/lib/ObjectSerializer.php       | 107 ---------------
 9 files changed, 88 insertions(+), 375 deletions(-)

diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/AnotherFakeApi.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/AnotherFakeApi.php
index 4d359a63b348..75672595b9ef 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/AnotherFakeApi.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/AnotherFakeApi.php
@@ -36,6 +36,7 @@
 use GuzzleHttp\RequestOptions;
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\HeaderSelector;
 use OpenAPI\Client\ObjectSerializer;
 
diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/DefaultApi.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/DefaultApi.php
index 34354fce4bd3..76576ecf0a1b 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/DefaultApi.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/DefaultApi.php
@@ -36,6 +36,7 @@
 use GuzzleHttp\RequestOptions;
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\HeaderSelector;
 use OpenAPI\Client\ObjectSerializer;
 
diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/FakeApi.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/FakeApi.php
index a89d506fec6e..4164e528f6a5 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/FakeApi.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/FakeApi.php
@@ -36,6 +36,7 @@
 use GuzzleHttp\RequestOptions;
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\HeaderSelector;
 use OpenAPI\Client\ObjectSerializer;
 
@@ -4566,69 +4567,27 @@ public function testEndpointParametersRequest($number, $double, $pattern_without
 
 
         // form params
-        if ($integer !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('integer', $integer));
-        }
-        // form params
-        if ($int32 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('int32', $int32));
-        }
-        // form params
-        if ($int64 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('int64', $int64));
-        }
-        // form params
-        if ($number !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('number', $number));
-        }
-        // form params
-        if ($float !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('float', $float));
-        }
-        // form params
-        if ($double !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('double', $double));
-        }
-        // form params
-        if ($string !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('string', $string));
-        }
-        // form params
-        if ($pattern_without_delimiter !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('pattern_without_delimiter', $pattern_without_delimiter));
-        }
-        // form params
-        if ($byte !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('byte', $byte));
-        }
-        // form params
-        if ($binary !== null) {
-            $multipart = true;
-            $formParams['binary'] = [];
-            $paramFiles = is_array($binary) ? $binary : [$binary];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['binary'][] = \GuzzleHttp\Psr7\Utils::tryFopen(
-                    ObjectSerializer::toFormValue('binary', $paramFile)['binary'],
-                    'rb'
-                );
-            }
-        }
-        // form params
-        if ($date !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('date', $date));
-        }
-        // form params
-        if ($date_time !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('dateTime', $date_time));
-        }
-        // form params
-        if ($password !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('password', $password));
-        }
-        // form params
-        if ($callback !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('callback', $callback));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'integer' => $integer,
+            'int32' => $int32,
+            'int64' => $int64,
+            'number' => $number,
+            'float' => $float,
+            'double' => $double,
+            'string' => $string,
+            'pattern_without_delimiter' => $pattern_without_delimiter,
+            'byte' => $byte,
+            'binary' => $binary,
+            'date' => $date,
+            'date_time' => $date_time,
+            'password' => $password,
+            'callback' => $callback,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
@@ -4941,13 +4900,15 @@ public function testEnumParametersRequest($enum_header_string_array = null, $enu
 
 
         // form params
-        if ($enum_form_string_array !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('enum_form_string_array', $enum_form_string_array));
-        }
-        // form params
-        if ($enum_form_string !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('enum_form_string', $enum_form_string));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'enum_form_string_array' => $enum_form_string_array,
+            'enum_form_string' => $enum_form_string,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
@@ -5919,13 +5880,15 @@ public function testJsonFormDataRequest($param, $param2, string $contentType = s
 
 
         // form params
-        if ($param !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('param', $param));
-        }
-        // form params
-        if ($param2 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('param2', $param2));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'param' => $param,
+            'param2' => $param2,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/FakeClassnameTags123Api.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/FakeClassnameTags123Api.php
index 6a1b0f92b79c..eca92c9723ba 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/FakeClassnameTags123Api.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/FakeClassnameTags123Api.php
@@ -36,6 +36,7 @@
 use GuzzleHttp\RequestOptions;
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\HeaderSelector;
 use OpenAPI\Client\ObjectSerializer;
 
diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php
index afc732bd153a..7e12927282e4 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php
@@ -36,6 +36,7 @@
 use GuzzleHttp\RequestOptions;
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\HeaderSelector;
 use OpenAPI\Client\ObjectSerializer;
 
@@ -2226,13 +2227,15 @@ public function updatePetWithFormRequest($pet_id, $name = null, $status = null,
         }
 
         // form params
-        if ($name !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('name', $name));
-        }
-        // form params
-        if ($status !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('status', $status));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'name' => $name,
+            'status' => $status,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
@@ -2558,21 +2561,15 @@ public function uploadFileRequest($pet_id, $additional_metadata = null, $file =
         }
 
         // form params
-        if ($additional_metadata !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('additionalMetadata', $additional_metadata));
-        }
-        // form params
-        if ($file !== null) {
-            $multipart = true;
-            $formParams['file'] = [];
-            $paramFiles = is_array($file) ? $file : [$file];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['file'][] = \GuzzleHttp\Psr7\Utils::tryFopen(
-                    ObjectSerializer::toFormValue('file', $paramFile)['file'],
-                    'rb'
-                );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'additional_metadata' => $additional_metadata,
+            'file' => $file,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $multipart = true;
         $headers = $this->headerSelector->selectHeaders(
@@ -2905,21 +2902,15 @@ public function uploadFileWithRequiredFileRequest($pet_id, $required_file, $addi
         }
 
         // form params
-        if ($additional_metadata !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('additionalMetadata', $additional_metadata));
-        }
-        // form params
-        if ($required_file !== null) {
-            $multipart = true;
-            $formParams['requiredFile'] = [];
-            $paramFiles = is_array($required_file) ? $required_file : [$required_file];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['requiredFile'][] = \GuzzleHttp\Psr7\Utils::tryFopen(
-                    ObjectSerializer::toFormValue('requiredFile', $paramFile)['requiredFile'],
-                    'rb'
-                );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'additional_metadata' => $additional_metadata,
+            'required_file' => $required_file,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $multipart = true;
         $headers = $this->headerSelector->selectHeaders(
@@ -3294,53 +3285,21 @@ public function uploadImageFullFormDataRequest($pet_id, $name, $photo_urls, $id
         }
 
         // form params
-        if ($id !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('id', $id));
-        }
-        // form params
-        if ($category !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('category', $category));
-        }
-        // form params
-        if ($name !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('name', $name));
-        }
-        // form params
-        if ($photo_urls !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('photoUrls', $photo_urls));
-        }
-        // form params
-        if ($tags !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('tags', $tags));
-        }
-        // form params
-        if ($status !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('status', $status));
-        }
-        // form params
-        if ($file !== null) {
-            $multipart = true;
-            $formParams['file'] = [];
-            $paramFiles = is_array($file) ? $file : [$file];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['file'][] = \GuzzleHttp\Psr7\Utils::tryFopen(
-                    ObjectSerializer::toFormValue('file', $paramFile)['file'],
-                    'rb'
-                );
-            }
-        }
-        // form params
-        if ($multiple_files !== null) {
-            $multipart = true;
-            $formParams['multiple_files'] = [];
-            $paramFiles = is_array($multiple_files) ? $multiple_files : [$multiple_files];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['multiple_files'][] = \GuzzleHttp\Psr7\Utils::tryFopen(
-                    ObjectSerializer::toFormValue('multiple_files', $paramFile)['multiple_files'],
-                    'rb'
-                );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'id' => $id,
+            'category' => $category,
+            'name' => $name,
+            'photo_urls' => $photo_urls,
+            'tags' => $tags,
+            'status' => $status,
+            'file' => $file,
+            'multiple_files' => $multiple_files,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $multipart = true;
         $headers = $this->headerSelector->selectHeaders(
diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/StoreApi.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/StoreApi.php
index 22cfae334214..bab21eea0931 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/StoreApi.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/StoreApi.php
@@ -36,6 +36,7 @@
 use GuzzleHttp\RequestOptions;
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\HeaderSelector;
 use OpenAPI\Client\ObjectSerializer;
 
diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/UserApi.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/UserApi.php
index ece4e248fa5d..b06b420cab02 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/UserApi.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/UserApi.php
@@ -36,6 +36,7 @@
 use GuzzleHttp\RequestOptions;
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\HeaderSelector;
 use OpenAPI\Client\ObjectSerializer;
 
diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/ObjectSerializer.php b/samples/client/petstore/php/OpenAPIClient-php/lib/ObjectSerializer.php
index 3e1cbe8778bc..e2ec7419e1a1 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/ObjectSerializer.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/ObjectSerializer.php
@@ -28,7 +28,6 @@
 
 namespace OpenAPI\Client;
 
-use ArrayAccess;
 use GuzzleHttp\Psr7\Utils;
 use OpenAPI\Client\Model\ModelInterface;
 
@@ -324,35 +323,6 @@ public static function toHeaderValue($value)
         return self::toString($value);
     }
 
-    /**
-     * Take value and turn it into an array suitable for inclusion in
-     * the http body (form parameter). If it's a string, pass through unchanged
-     * If it's a datetime object, format it in ISO8601
-     *
-     * @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter
-     *
-     * @return array [key => value] of formdata
-     */
-    public static function toFormValue(string $key, mixed $value)
-    {
-        if ($value instanceof \SplFileObject) {
-            return [$key => $value->getRealPath()];
-        } elseif (is_array($value) || $value instanceof ArrayAccess) {
-            $flattened = [];
-            $result = [];
-
-            self::flattenArray(json_decode(json_encode($value), true), $flattened);
-
-            foreach ($flattened as $k => $v) {
-                $result["{$key}{$k}"] = self::toString($v);
-            }
-
-            return $result;
-        } else {
-            return [$key => self::toString($value)];
-        }
-    }
-
     /**
      * Take value and turn it into a string suitable for inclusion in
      * the parameter. If it's a string, pass through unchanged
@@ -626,81 +596,4 @@ public static function buildQuery(array $params, $encoding = PHP_QUERY_RFC3986):
 
         return $qs ? (string) substr($qs, 0, -1) : '';
     }
-
-    /**
-     * Flattens an array of Model object and generates an array compatible
-     * with formdata - a single-level array where the keys use bracket
-     * notation to signify nested data.
-     *
-     * @param \ArrayAccess|array $source
-     *
-     * credit: https://github.com/FranBar1966/FlatPHP
-     */
-    private static function flattenArray(
-        mixed $source,
-        array &$destination,
-        string $start = '',
-    ) {
-        $opt = [
-            'prefix'          => '[',
-            'suffix'          => ']',
-            'suffix-end'      => true,
-            'prefix-list'     => '[',
-            'suffix-list'     => ']',
-            'suffix-list-end' => true,
-        ];
-
-        if (!is_array($source)) {
-            $source = (array) $source;
-        }
-
-        /**
-         * array_is_list only in PHP >= 8.1
-         *
-         * credit: https://www.php.net/manual/en/function.array-is-list.php#127044
-         */
-        if (!function_exists('array_is_list')) {
-            function array_is_list(array $array)
-            {
-                $i = -1;
-
-                foreach ($array as $k => $v) {
-                    ++$i;
-                    if ($k !== $i) {
-                        return false;
-                    }
-                }
-
-                return true;
-            }
-        }
-
-        if (array_is_list($source)) {
-            $currentPrefix    = $opt['prefix-list'];
-            $currentSuffix    = $opt['suffix-list'];
-            $currentSuffixEnd = $opt['suffix-list-end'];
-        } else {
-            $currentPrefix    = $opt['prefix'];
-            $currentSuffix    = $opt['suffix'];
-            $currentSuffixEnd = $opt['suffix-end'];
-        }
-
-        $currentName = $start;
-
-        foreach ($source as $key => $val) {
-            $currentName .= $currentPrefix.$key;
-
-            if (is_array($val) && !empty($val)) {
-                $currentName .= "{$currentSuffix}";
-                self::flattenArray($val, $destination, $currentName);
-            } else {
-                if ($currentSuffixEnd) {
-                    $currentName .= $currentSuffix;
-                }
-                $destination[$currentName] = self::toString($val);
-            }
-
-            $currentName = $start;
-        }
-    }
 }
diff --git a/samples/client/petstore/php/psr-18/lib/ObjectSerializer.php b/samples/client/petstore/php/psr-18/lib/ObjectSerializer.php
index 3e1cbe8778bc..e2ec7419e1a1 100644
--- a/samples/client/petstore/php/psr-18/lib/ObjectSerializer.php
+++ b/samples/client/petstore/php/psr-18/lib/ObjectSerializer.php
@@ -28,7 +28,6 @@
 
 namespace OpenAPI\Client;
 
-use ArrayAccess;
 use GuzzleHttp\Psr7\Utils;
 use OpenAPI\Client\Model\ModelInterface;
 
@@ -324,35 +323,6 @@ public static function toHeaderValue($value)
         return self::toString($value);
     }
 
-    /**
-     * Take value and turn it into an array suitable for inclusion in
-     * the http body (form parameter). If it's a string, pass through unchanged
-     * If it's a datetime object, format it in ISO8601
-     *
-     * @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter
-     *
-     * @return array [key => value] of formdata
-     */
-    public static function toFormValue(string $key, mixed $value)
-    {
-        if ($value instanceof \SplFileObject) {
-            return [$key => $value->getRealPath()];
-        } elseif (is_array($value) || $value instanceof ArrayAccess) {
-            $flattened = [];
-            $result = [];
-
-            self::flattenArray(json_decode(json_encode($value), true), $flattened);
-
-            foreach ($flattened as $k => $v) {
-                $result["{$key}{$k}"] = self::toString($v);
-            }
-
-            return $result;
-        } else {
-            return [$key => self::toString($value)];
-        }
-    }
-
     /**
      * Take value and turn it into a string suitable for inclusion in
      * the parameter. If it's a string, pass through unchanged
@@ -626,81 +596,4 @@ public static function buildQuery(array $params, $encoding = PHP_QUERY_RFC3986):
 
         return $qs ? (string) substr($qs, 0, -1) : '';
     }
-
-    /**
-     * Flattens an array of Model object and generates an array compatible
-     * with formdata - a single-level array where the keys use bracket
-     * notation to signify nested data.
-     *
-     * @param \ArrayAccess|array $source
-     *
-     * credit: https://github.com/FranBar1966/FlatPHP
-     */
-    private static function flattenArray(
-        mixed $source,
-        array &$destination,
-        string $start = '',
-    ) {
-        $opt = [
-            'prefix'          => '[',
-            'suffix'          => ']',
-            'suffix-end'      => true,
-            'prefix-list'     => '[',
-            'suffix-list'     => ']',
-            'suffix-list-end' => true,
-        ];
-
-        if (!is_array($source)) {
-            $source = (array) $source;
-        }
-
-        /**
-         * array_is_list only in PHP >= 8.1
-         *
-         * credit: https://www.php.net/manual/en/function.array-is-list.php#127044
-         */
-        if (!function_exists('array_is_list')) {
-            function array_is_list(array $array)
-            {
-                $i = -1;
-
-                foreach ($array as $k => $v) {
-                    ++$i;
-                    if ($k !== $i) {
-                        return false;
-                    }
-                }
-
-                return true;
-            }
-        }
-
-        if (array_is_list($source)) {
-            $currentPrefix    = $opt['prefix-list'];
-            $currentSuffix    = $opt['suffix-list'];
-            $currentSuffixEnd = $opt['suffix-list-end'];
-        } else {
-            $currentPrefix    = $opt['prefix'];
-            $currentSuffix    = $opt['suffix'];
-            $currentSuffixEnd = $opt['suffix-end'];
-        }
-
-        $currentName = $start;
-
-        foreach ($source as $key => $val) {
-            $currentName .= $currentPrefix.$key;
-
-            if (is_array($val) && !empty($val)) {
-                $currentName .= "{$currentSuffix}";
-                self::flattenArray($val, $destination, $currentName);
-            } else {
-                if ($currentSuffixEnd) {
-                    $currentName .= $currentSuffix;
-                }
-                $destination[$currentName] = self::toString($val);
-            }
-
-            $currentName = $start;
-        }
-    }
 }

From 05eff054f7fa8d01a740963be4a236b0d2a7de61 Mon Sep 17 00:00:00 2001
From: Juan Treminio <jtreminio@gmail.com>
Date: Fri, 28 Mar 2025 18:08:45 -0500
Subject: [PATCH 03/10] Updates php-nextgen and psr-18

---
 .../languages/PhpNextgenClientCodegen.java    |   1 +
 .../php-nextgen/FormDataProcessor.mustache    | 227 ++++++++++++++++++
 .../php-nextgen/ObjectSerializer.mustache     |  86 -------
 .../main/resources/php-nextgen/api.mustache   |  31 +--
 .../resources/php/FormDataProcessor.mustache  |   5 +-
 .../php/libraries/psr-18/api.mustache         |  29 +--
 .../php-nextgen-streaming/src/Api/AuthApi.php |   1 +
 .../php-nextgen-streaming/src/Api/BodyApi.php |  43 ++--
 .../php-nextgen-streaming/src/Api/FormApi.php |  69 +++---
 .../src/Api/HeaderApi.php                     |   1 +
 .../php-nextgen-streaming/src/Api/PathApi.php |   1 +
 .../src/Api/QueryApi.php                      |   1 +
 .../src/ObjectSerializer.php                  |  86 -------
 .../echo_api/php-nextgen/src/Api/AuthApi.php  |   1 +
 .../echo_api/php-nextgen/src/Api/BodyApi.php  |  43 ++--
 .../echo_api/php-nextgen/src/Api/FormApi.php  |  69 +++---
 .../php-nextgen/src/Api/HeaderApi.php         |   1 +
 .../echo_api/php-nextgen/src/Api/PathApi.php  |   1 +
 .../echo_api/php-nextgen/src/Api/QueryApi.php |   1 +
 .../php-nextgen/src/ObjectSerializer.php      |  86 -------
 .../src/Api/AnotherFakeApi.php                |   1 +
 .../OpenAPIClient-php/src/Api/DefaultApi.php  |   1 +
 .../OpenAPIClient-php/src/Api/FakeApi.php     | 119 +++------
 .../src/Api/FakeClassnameTags123Api.php       |   1 +
 .../OpenAPIClient-php/src/Api/PetApi.php      |  69 +++---
 .../OpenAPIClient-php/src/Api/StoreApi.php    |   1 +
 .../OpenAPIClient-php/src/Api/UserApi.php     |   1 +
 .../src/ObjectSerializer.php                  |  86 -------
 .../php/psr-18/lib/Api/AnotherFakeApi.php     |   1 +
 .../php/psr-18/lib/Api/DefaultApi.php         |   1 +
 .../petstore/php/psr-18/lib/Api/FakeApi.php   | 117 +++------
 .../lib/Api/FakeClassnameTags123Api.php       |   1 +
 .../petstore/php/psr-18/lib/Api/PetApi.php    | 127 ++++------
 .../petstore/php/psr-18/lib/Api/StoreApi.php  |   1 +
 .../petstore/php/psr-18/lib/Api/UserApi.php   |   1 +
 35 files changed, 523 insertions(+), 788 deletions(-)
 create mode 100644 modules/openapi-generator/src/main/resources/php-nextgen/FormDataProcessor.mustache

diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpNextgenClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpNextgenClientCodegen.java
index 64e8c94b28c2..b24127146811 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpNextgenClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpNextgenClientCodegen.java
@@ -121,6 +121,7 @@ public void processOpts() {
 
         supportingFiles.add(new SupportingFile("ApiException.mustache", toSrcPath(invokerPackage, srcBasePath), "ApiException.php"));
         supportingFiles.add(new SupportingFile("Configuration.mustache", toSrcPath(invokerPackage, srcBasePath), "Configuration.php"));
+        supportingFiles.add(new SupportingFile("FormDataProcessor.mustache", toSrcPath(invokerPackage, srcBasePath), "FormDataProcessor.php"));
         supportingFiles.add(new SupportingFile("ObjectSerializer.mustache", toSrcPath(invokerPackage, srcBasePath), "ObjectSerializer.php"));
         supportingFiles.add(new SupportingFile("ModelInterface.mustache", toSrcPath(modelPackage, srcBasePath), "ModelInterface.php"));
         supportingFiles.add(new SupportingFile("HeaderSelector.mustache", toSrcPath(invokerPackage, srcBasePath), "HeaderSelector.php"));
diff --git a/modules/openapi-generator/src/main/resources/php-nextgen/FormDataProcessor.mustache b/modules/openapi-generator/src/main/resources/php-nextgen/FormDataProcessor.mustache
new file mode 100644
index 000000000000..ad9808c4ab12
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/php-nextgen/FormDataProcessor.mustache
@@ -0,0 +1,227 @@
+<?php
+/**
+ * FormDataProcessor
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  {{invokerPackage}}
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+{{>partial_header}}
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace {{invokerPackage}};
+
+use ArrayAccess;
+use DateTime;
+use GuzzleHttp\Psr7\Utils;
+use Psr\Http\Message\StreamInterface;
+use SplFileObject;
+use {{modelPackage}}\ModelInterface;
+
+class FormDataProcessor
+{
+    /**
+     * Tags whether payload passed to ::prepare() contains one or more
+     * SplFileObject or stream values.
+     */
+    public bool $has_file = false;
+
+    /**
+     * Take value and turn it into an array suitable for inclusion in
+     * the http body (form parameter). If it's a string, pass through unchanged
+     * If it's a datetime object, format it in ISO8601
+     *
+     * @param array<string|bool|array|DateTime|ArrayAccess|SplFileObject> $values the value of the form parameter
+     *
+     * @return array [key => value] of formdata
+     */
+    public function prepare(array $values): array
+    {
+        $this->has_file = false;
+        $result = [];
+
+        foreach ($values as $k => $v) {
+            if ($v === null) {
+                continue;
+            }
+
+            $result[$k] = $this->makeFormSafe($v);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Flattens a multi-level array of data and generates a single-level array
+     * compatible with formdata - a single-level array where the keys use bracket
+     * notation to signify nested data.
+     *
+     * credit: https://github.com/FranBar1966/FlatPHP
+     */
+    public static function flatten(array $source, string $start = ''): array
+    {
+        $opt = [
+            'prefix'          => '[',
+            'suffix'          => ']',
+            'suffix-end'      => true,
+            'prefix-list'     => '[',
+            'suffix-list'     => ']',
+            'suffix-list-end' => true,
+        ];
+
+        if ($start === '') {
+            $currentPrefix = '';
+            $currentSuffix = '';
+            $currentSuffixEnd = false;
+        } elseif (array_is_list($source)) {
+            $currentPrefix = $opt['prefix-list'];
+            $currentSuffix = $opt['suffix-list'];
+            $currentSuffixEnd = $opt['suffix-list-end'];
+        } else {
+            $currentPrefix = $opt['prefix'];
+            $currentSuffix = $opt['suffix'];
+            $currentSuffixEnd = $opt['suffix-end'];
+        }
+
+        $currentName = $start;
+        $result = [];
+
+        foreach ($source as $key => $val) {
+            $currentName .= $currentPrefix . $key;
+
+            if (is_array($val) && !empty($val)) {
+                $currentName .= $currentSuffix;
+                $result += self::flatten($val, $currentName);
+            } else {
+                if ($currentSuffixEnd) {
+                    $currentName .= $currentSuffix;
+                }
+
+                $result[$currentName] = ObjectSerializer::toString($val);
+            }
+
+            $currentName = $start;
+        }
+
+        return $result;
+    }
+
+    /**
+     * formdata must be limited to scalars or arrays of scalar values,
+     * or a resource for a file upload. Here we iterate through all available
+     * data and identify how to handle each scenario
+     *
+     * @param string|bool|array|DateTime|ArrayAccess|SplFileObject $value
+     */
+    protected function makeFormSafe(mixed $value)
+    {
+        if ($value instanceof SplFileObject) {
+            return $this->processFiles([$value])[0];
+        }
+
+        if (is_resource($value)) {
+            $this->has_file = true;
+
+            return $value;
+        }
+
+        if ($value instanceof ModelInterface) {
+            return $this->processModel($value);
+        }
+
+        if (is_array($value) || is_object($value)) {
+            $data = [];
+
+            foreach ($value as $k => $v) {
+                $data[$k] = $this->makeFormSafe($v);
+            }
+
+            return $data;
+        }
+
+        return ObjectSerializer::toString($value);
+    }
+
+    /**
+     * We are able to handle nested ModelInterface. We do not simply call
+     * json_decode(json_encode()) because any given model may have binary data
+     * or other data that cannot be serialized to a JSON string
+     */
+    protected function processModel(ModelInterface $model): array
+    {
+        $result = [];
+
+        foreach ($model::openAPITypes() as $name => $type) {
+            $value = $model->offsetGet($name);
+
+            if ($value === null) {
+                continue;
+            }
+
+            if (str_contains($type, '\SplFileObject')) {
+                $file = is_array($value) ? $value : [$value];
+                $result[$name] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($value instanceof ModelInterface) {
+                $result[$name] = $this->processModel($value);
+
+                continue;
+            }
+
+            if (is_array($value) || is_object($value)) {
+                $result[$name] = $this->makeFormSafe($value);
+
+                continue;
+            }
+
+            $result[$name] = ObjectSerializer::toString($value);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Handle file data
+     */
+    protected function processFiles(array $files): array
+    {
+        $this->has_file = true;
+
+        $result = [];
+
+        foreach ($files as $i => $file) {
+            if (is_array($file)) {
+                $result[$i] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($file instanceof StreamInterface) {
+                $result[$i] = $file;
+
+                continue;
+            }
+
+            if ($file instanceof SplFileObject) {
+                $result[$i] = $this->tryFopen($file);
+            }
+        }
+
+        return $result;
+    }
+
+    private function tryFopen(SplFileObject $file)
+    {
+        return Utils::tryFopen($file->getRealPath(), 'rb');
+    }
+}
diff --git a/modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache b/modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache
index 81dd8a50387f..32a77bef4bac 100644
--- a/modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache
+++ b/modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache
@@ -18,7 +18,6 @@
 
 namespace {{invokerPackage}};
 
-use ArrayAccess;
 use DateTimeInterface;
 use DateTime;
 use GuzzleHttp\Psr7\Utils;
@@ -316,37 +315,6 @@ class ObjectSerializer
         return self::toString($value);
     }
 
-    /**
-     * Take value and turn it into an array suitable for inclusion in
-     * the http body (form parameter). If it's a string, pass through unchanged
-     * If it's a datetime object, format it in ISO8601
-     *
-     * @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter
-     *
-     * @return array [key => value] of formdata
-     */
-    public static function toFormValue(
-        string $key,
-        string|bool|array|DateTime|ArrayAccess|\SplFileObject $value,
-    ): array {
-        if ($value instanceof \SplFileObject) {
-            return [$key => $value->getRealPath()];
-        } elseif (is_array($value) || $value instanceof ArrayAccess) {
-            $flattened = [];
-            $result = [];
-
-            self::flattenArray(json_decode(json_encode($value), true), $flattened);
-
-            foreach ($flattened as $k => $v) {
-                $result["{$key}{$k}"] = self::toString($v);
-            }
-
-            return $result;
-        } else {
-            return [$key => self::toString($value)];
-        }
-    }
-
     /**
      * Take value and turn it into a string suitable for inclusion in
      * the parameter. If it's a string, pass through unchanged
@@ -612,58 +580,4 @@ class ObjectSerializer
 
         return $qs ? (string) substr($qs, 0, -1) : '';
     }
-
-    /**
-     * Flattens an array of Model object and generates an array compatible
-     * with formdata - a single-level array where the keys use bracket
-     * notation to signify nested data.
-     *
-     * credit: https://github.com/FranBar1966/FlatPHP
-     */
-    private static function flattenArray(
-        ArrayAccess|array $source,
-        array &$destination,
-        string $start = '',
-    ) {
-        $opt = [
-            'prefix'          => '[',
-            'suffix'          => ']',
-            'suffix-end'      => true,
-            'prefix-list'     => '[',
-            'suffix-list'     => ']',
-            'suffix-list-end' => true,
-        ];
-
-        if (!is_array($source)) {
-            $source = (array) $source;
-        }
-
-        if (array_is_list($source)) {
-            $currentPrefix    = $opt['prefix-list'];
-            $currentSuffix    = $opt['suffix-list'];
-            $currentSuffixEnd = $opt['suffix-list-end'];
-        } else {
-            $currentPrefix    = $opt['prefix'];
-            $currentSuffix    = $opt['suffix'];
-            $currentSuffixEnd = $opt['suffix-end'];
-        }
-
-        $currentName = $start;
-
-        foreach ($source as $key => $val) {
-            $currentName .= $currentPrefix.$key;
-
-            if (is_array($val) && !empty($val)) {
-                $currentName .= "{$currentSuffix}";
-                self::flattenArray($val, $destination, $currentName);
-            } else {
-                if ($currentSuffixEnd) {
-                    $currentName .= $currentSuffix;
-                }
-                $destination[$currentName] = self::toString($val);
-            }
-
-            $currentName = $start;
-        }
-    }
 }
diff --git a/modules/openapi-generator/src/main/resources/php-nextgen/api.mustache b/modules/openapi-generator/src/main/resources/php-nextgen/api.mustache
index 96c394a31bb0..2161f88a8864 100644
--- a/modules/openapi-generator/src/main/resources/php-nextgen/api.mustache
+++ b/modules/openapi-generator/src/main/resources/php-nextgen/api.mustache
@@ -29,6 +29,7 @@ use GuzzleHttp\Promise\PromiseInterface;
 use {{invokerPackage}}\ApiException;
 use {{invokerPackage}}\Configuration;
 use {{invokerPackage}}\HeaderSelector;
+use {{invokerPackage}}\FormDataProcessor;
 use {{invokerPackage}}\ObjectSerializer;
 
 /**
@@ -749,25 +750,19 @@ use {{invokerPackage}}\ObjectSerializer;
         {{/pathParams}}
 
         {{#formParams}}
+        {{#-first}}
         // form params
-        if (${{paramName}} !== null) {
-            {{#isFile}}
-            $multipart = true;
-            $formParams['{{baseName}}'] = [];
-            $paramFiles = is_array(${{paramName}}) ? ${{paramName}} : [${{paramName}}];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['{{baseName}}'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
-                    ? $paramFile
-                    : \GuzzleHttp\Psr7\Utils::tryFopen(
-                        ObjectSerializer::toFormValue('{{baseName}}', $paramFile)['{{baseName}}'],
-                        'rb'
-                    );
-            }
-            {{/isFile}}
-            {{^isFile}}
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('{{baseName}}', ${{paramName}}));
-            {{/isFile}}
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+        {{/-first}}
+            '{{paramName}}' => ${{paramName}},
+        {{#-last}}
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
+        {{/-last}}
         {{/formParams}}
 
         $headers = $this->headerSelector->selectHeaders(
diff --git a/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache b/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache
index 3db913860835..89751edb321a 100644
--- a/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache
+++ b/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache
@@ -1,7 +1,6 @@
 <?php
 /**
- * ObjectSerializer
- *
+ * FormDataProcessor
  * PHP version 7.4
  *
  * @category Class
@@ -47,7 +46,7 @@ class FormDataProcessor
      * the http body (form parameter). If it's a string, pass through unchanged
      * If it's a datetime object, format it in ISO8601
      *
-     * @param string|bool|array|DateTime|ArrayAccess|SplFileObject $values the value of the form parameter
+     * @param array<string|bool|array|DateTime|ArrayAccess|SplFileObject> $values the value of the form parameter
      *
      * @return array [key => value] of formdata
      */
diff --git a/modules/openapi-generator/src/main/resources/php/libraries/psr-18/api.mustache b/modules/openapi-generator/src/main/resources/php/libraries/psr-18/api.mustache
index 16e76da9074c..1b47bd197269 100644
--- a/modules/openapi-generator/src/main/resources/php/libraries/psr-18/api.mustache
+++ b/modules/openapi-generator/src/main/resources/php/libraries/psr-18/api.mustache
@@ -34,6 +34,7 @@ use {{invokerPackage}}\ApiException;
 use {{invokerPackage}}\Configuration;
 use {{invokerPackage}}\DebugPlugin;
 use {{invokerPackage}}\HeaderSelector;
+use {{invokerPackage}}\FormDataProcessor;
 use {{invokerPackage}}\ObjectSerializer;
 use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Client\ClientInterface;
@@ -585,23 +586,19 @@ use function sprintf;
         {{/pathParams}}
 
         {{#formParams}}
+        {{#-first}}
         // form params
-        if (${{paramName}} !== null) {
-            {{#isFile}}
-            $multipart = true;
-            $formParams['{{baseName}}'] = [];
-            $paramFiles = is_array(${{paramName}}) ? ${{paramName}} : [${{paramName}}];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['{{baseName}}'][] = \GuzzleHttp\Psr7\try_fopen(
-                    ObjectSerializer::toFormValue('{{baseName}}', $paramFile)['{{baseName}}'],
-                    'rb'
-                );
-            }
-            {{/isFile}}
-            {{^isFile}}
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('{{baseName}}', ${{paramName}}));
-            {{/isFile}}
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+        {{/-first}}
+            '{{paramName}}' => ${{paramName}},
+        {{#-last}}
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
+        {{/-last}}
         {{/formParams}}
 
         $headers = $this->headerSelector->selectHeaders(
diff --git a/samples/client/echo_api/php-nextgen-streaming/src/Api/AuthApi.php b/samples/client/echo_api/php-nextgen-streaming/src/Api/AuthApi.php
index 2ab4f17c90c4..969961bd61a2 100644
--- a/samples/client/echo_api/php-nextgen-streaming/src/Api/AuthApi.php
+++ b/samples/client/echo_api/php-nextgen-streaming/src/Api/AuthApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/echo_api/php-nextgen-streaming/src/Api/BodyApi.php b/samples/client/echo_api/php-nextgen-streaming/src/Api/BodyApi.php
index 78c0645f1ad0..dd55eba1fb0e 100644
--- a/samples/client/echo_api/php-nextgen-streaming/src/Api/BodyApi.php
+++ b/samples/client/echo_api/php-nextgen-streaming/src/Api/BodyApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
@@ -1026,19 +1027,14 @@ public function testBodyMultipartFormdataArrayOfBinaryRequest(
 
 
         // form params
-        if ($files !== null) {
-            $multipart = true;
-            $formParams['files'] = [];
-            $paramFiles = is_array($files) ? $files : [$files];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['files'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
-                    ? $paramFile
-                    : \GuzzleHttp\Psr7\Utils::tryFopen(
-                        ObjectSerializer::toFormValue('files', $paramFile)['files'],
-                        'rb'
-                    );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'files' => $files,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['text/plain', ],
@@ -1349,19 +1345,14 @@ public function testBodyMultipartFormdataSingleBinaryRequest(
 
 
         // form params
-        if ($my_file !== null) {
-            $multipart = true;
-            $formParams['my-file'] = [];
-            $paramFiles = is_array($my_file) ? $my_file : [$my_file];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['my-file'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
-                    ? $paramFile
-                    : \GuzzleHttp\Psr7\Utils::tryFopen(
-                        ObjectSerializer::toFormValue('my-file', $paramFile)['my-file'],
-                        'rb'
-                    );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'my_file' => $my_file,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['text/plain', ],
diff --git a/samples/client/echo_api/php-nextgen-streaming/src/Api/FormApi.php b/samples/client/echo_api/php-nextgen-streaming/src/Api/FormApi.php
index 193033455ace..39aadcbd1e18 100644
--- a/samples/client/echo_api/php-nextgen-streaming/src/Api/FormApi.php
+++ b/samples/client/echo_api/php-nextgen-streaming/src/Api/FormApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
@@ -407,17 +408,16 @@ public function testFormIntegerBooleanStringRequest(
 
 
         // form params
-        if ($integer_form !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('integer_form', $integer_form));
-        }
-        // form params
-        if ($boolean_form !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('boolean_form', $boolean_form));
-        }
-        // form params
-        if ($string_form !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('string_form', $string_form));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'integer_form' => $integer_form,
+            'boolean_form' => $boolean_form,
+            'string_form' => $string_form,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['text/plain', ],
@@ -734,9 +734,14 @@ public function testFormObjectMultipartRequest(
 
 
         // form params
-        if ($marker !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('marker', $marker));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'marker' => $marker,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['text/plain', ],
@@ -1102,29 +1107,19 @@ public function testFormOneofRequest(
 
 
         // form params
-        if ($form1 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('form1', $form1));
-        }
-        // form params
-        if ($form2 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('form2', $form2));
-        }
-        // form params
-        if ($form3 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('form3', $form3));
-        }
-        // form params
-        if ($form4 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('form4', $form4));
-        }
-        // form params
-        if ($id !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('id', $id));
-        }
-        // form params
-        if ($name !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('name', $name));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'form1' => $form1,
+            'form2' => $form2,
+            'form3' => $form3,
+            'form4' => $form4,
+            'id' => $id,
+            'name' => $name,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['text/plain', ],
diff --git a/samples/client/echo_api/php-nextgen-streaming/src/Api/HeaderApi.php b/samples/client/echo_api/php-nextgen-streaming/src/Api/HeaderApi.php
index aa4c1fcbdc9a..0ae433ed4eb6 100644
--- a/samples/client/echo_api/php-nextgen-streaming/src/Api/HeaderApi.php
+++ b/samples/client/echo_api/php-nextgen-streaming/src/Api/HeaderApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/echo_api/php-nextgen-streaming/src/Api/PathApi.php b/samples/client/echo_api/php-nextgen-streaming/src/Api/PathApi.php
index 5fb253899d02..1e5785253fce 100644
--- a/samples/client/echo_api/php-nextgen-streaming/src/Api/PathApi.php
+++ b/samples/client/echo_api/php-nextgen-streaming/src/Api/PathApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/echo_api/php-nextgen-streaming/src/Api/QueryApi.php b/samples/client/echo_api/php-nextgen-streaming/src/Api/QueryApi.php
index 12169d7ef19b..5fcca472ffe3 100644
--- a/samples/client/echo_api/php-nextgen-streaming/src/Api/QueryApi.php
+++ b/samples/client/echo_api/php-nextgen-streaming/src/Api/QueryApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php b/samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php
index ac7cf8884fd6..135df37659ed 100644
--- a/samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php
+++ b/samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php
@@ -28,7 +28,6 @@
 
 namespace OpenAPI\Client;
 
-use ArrayAccess;
 use DateTimeInterface;
 use DateTime;
 use GuzzleHttp\Psr7\Utils;
@@ -326,37 +325,6 @@ public static function toHeaderValue(string $value): string
         return self::toString($value);
     }
 
-    /**
-     * Take value and turn it into an array suitable for inclusion in
-     * the http body (form parameter). If it's a string, pass through unchanged
-     * If it's a datetime object, format it in ISO8601
-     *
-     * @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter
-     *
-     * @return array [key => value] of formdata
-     */
-    public static function toFormValue(
-        string $key,
-        string|bool|array|DateTime|ArrayAccess|\SplFileObject $value,
-    ): array {
-        if ($value instanceof \SplFileObject) {
-            return [$key => $value->getRealPath()];
-        } elseif (is_array($value) || $value instanceof ArrayAccess) {
-            $flattened = [];
-            $result = [];
-
-            self::flattenArray(json_decode(json_encode($value), true), $flattened);
-
-            foreach ($flattened as $k => $v) {
-                $result["{$key}{$k}"] = self::toString($v);
-            }
-
-            return $result;
-        } else {
-            return [$key => self::toString($value)];
-        }
-    }
-
     /**
      * Take value and turn it into a string suitable for inclusion in
      * the parameter. If it's a string, pass through unchanged
@@ -622,58 +590,4 @@ public static function buildQuery(array $params, $encoding = PHP_QUERY_RFC3986):
 
         return $qs ? (string) substr($qs, 0, -1) : '';
     }
-
-    /**
-     * Flattens an array of Model object and generates an array compatible
-     * with formdata - a single-level array where the keys use bracket
-     * notation to signify nested data.
-     *
-     * credit: https://github.com/FranBar1966/FlatPHP
-     */
-    private static function flattenArray(
-        ArrayAccess|array $source,
-        array &$destination,
-        string $start = '',
-    ) {
-        $opt = [
-            'prefix'          => '[',
-            'suffix'          => ']',
-            'suffix-end'      => true,
-            'prefix-list'     => '[',
-            'suffix-list'     => ']',
-            'suffix-list-end' => true,
-        ];
-
-        if (!is_array($source)) {
-            $source = (array) $source;
-        }
-
-        if (array_is_list($source)) {
-            $currentPrefix    = $opt['prefix-list'];
-            $currentSuffix    = $opt['suffix-list'];
-            $currentSuffixEnd = $opt['suffix-list-end'];
-        } else {
-            $currentPrefix    = $opt['prefix'];
-            $currentSuffix    = $opt['suffix'];
-            $currentSuffixEnd = $opt['suffix-end'];
-        }
-
-        $currentName = $start;
-
-        foreach ($source as $key => $val) {
-            $currentName .= $currentPrefix.$key;
-
-            if (is_array($val) && !empty($val)) {
-                $currentName .= "{$currentSuffix}";
-                self::flattenArray($val, $destination, $currentName);
-            } else {
-                if ($currentSuffixEnd) {
-                    $currentName .= $currentSuffix;
-                }
-                $destination[$currentName] = self::toString($val);
-            }
-
-            $currentName = $start;
-        }
-    }
 }
diff --git a/samples/client/echo_api/php-nextgen/src/Api/AuthApi.php b/samples/client/echo_api/php-nextgen/src/Api/AuthApi.php
index 2ab4f17c90c4..969961bd61a2 100644
--- a/samples/client/echo_api/php-nextgen/src/Api/AuthApi.php
+++ b/samples/client/echo_api/php-nextgen/src/Api/AuthApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/echo_api/php-nextgen/src/Api/BodyApi.php b/samples/client/echo_api/php-nextgen/src/Api/BodyApi.php
index c2459f1777eb..4e0ad3a30218 100644
--- a/samples/client/echo_api/php-nextgen/src/Api/BodyApi.php
+++ b/samples/client/echo_api/php-nextgen/src/Api/BodyApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
@@ -1026,19 +1027,14 @@ public function testBodyMultipartFormdataArrayOfBinaryRequest(
 
 
         // form params
-        if ($files !== null) {
-            $multipart = true;
-            $formParams['files'] = [];
-            $paramFiles = is_array($files) ? $files : [$files];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['files'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
-                    ? $paramFile
-                    : \GuzzleHttp\Psr7\Utils::tryFopen(
-                        ObjectSerializer::toFormValue('files', $paramFile)['files'],
-                        'rb'
-                    );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'files' => $files,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['text/plain', ],
@@ -1349,19 +1345,14 @@ public function testBodyMultipartFormdataSingleBinaryRequest(
 
 
         // form params
-        if ($my_file !== null) {
-            $multipart = true;
-            $formParams['my-file'] = [];
-            $paramFiles = is_array($my_file) ? $my_file : [$my_file];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['my-file'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
-                    ? $paramFile
-                    : \GuzzleHttp\Psr7\Utils::tryFopen(
-                        ObjectSerializer::toFormValue('my-file', $paramFile)['my-file'],
-                        'rb'
-                    );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'my_file' => $my_file,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['text/plain', ],
diff --git a/samples/client/echo_api/php-nextgen/src/Api/FormApi.php b/samples/client/echo_api/php-nextgen/src/Api/FormApi.php
index 193033455ace..39aadcbd1e18 100644
--- a/samples/client/echo_api/php-nextgen/src/Api/FormApi.php
+++ b/samples/client/echo_api/php-nextgen/src/Api/FormApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
@@ -407,17 +408,16 @@ public function testFormIntegerBooleanStringRequest(
 
 
         // form params
-        if ($integer_form !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('integer_form', $integer_form));
-        }
-        // form params
-        if ($boolean_form !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('boolean_form', $boolean_form));
-        }
-        // form params
-        if ($string_form !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('string_form', $string_form));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'integer_form' => $integer_form,
+            'boolean_form' => $boolean_form,
+            'string_form' => $string_form,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['text/plain', ],
@@ -734,9 +734,14 @@ public function testFormObjectMultipartRequest(
 
 
         // form params
-        if ($marker !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('marker', $marker));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'marker' => $marker,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['text/plain', ],
@@ -1102,29 +1107,19 @@ public function testFormOneofRequest(
 
 
         // form params
-        if ($form1 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('form1', $form1));
-        }
-        // form params
-        if ($form2 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('form2', $form2));
-        }
-        // form params
-        if ($form3 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('form3', $form3));
-        }
-        // form params
-        if ($form4 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('form4', $form4));
-        }
-        // form params
-        if ($id !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('id', $id));
-        }
-        // form params
-        if ($name !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('name', $name));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'form1' => $form1,
+            'form2' => $form2,
+            'form3' => $form3,
+            'form4' => $form4,
+            'id' => $id,
+            'name' => $name,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['text/plain', ],
diff --git a/samples/client/echo_api/php-nextgen/src/Api/HeaderApi.php b/samples/client/echo_api/php-nextgen/src/Api/HeaderApi.php
index aa4c1fcbdc9a..0ae433ed4eb6 100644
--- a/samples/client/echo_api/php-nextgen/src/Api/HeaderApi.php
+++ b/samples/client/echo_api/php-nextgen/src/Api/HeaderApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/echo_api/php-nextgen/src/Api/PathApi.php b/samples/client/echo_api/php-nextgen/src/Api/PathApi.php
index 5fb253899d02..1e5785253fce 100644
--- a/samples/client/echo_api/php-nextgen/src/Api/PathApi.php
+++ b/samples/client/echo_api/php-nextgen/src/Api/PathApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/echo_api/php-nextgen/src/Api/QueryApi.php b/samples/client/echo_api/php-nextgen/src/Api/QueryApi.php
index 12169d7ef19b..5fcca472ffe3 100644
--- a/samples/client/echo_api/php-nextgen/src/Api/QueryApi.php
+++ b/samples/client/echo_api/php-nextgen/src/Api/QueryApi.php
@@ -39,6 +39,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/echo_api/php-nextgen/src/ObjectSerializer.php b/samples/client/echo_api/php-nextgen/src/ObjectSerializer.php
index ac7cf8884fd6..135df37659ed 100644
--- a/samples/client/echo_api/php-nextgen/src/ObjectSerializer.php
+++ b/samples/client/echo_api/php-nextgen/src/ObjectSerializer.php
@@ -28,7 +28,6 @@
 
 namespace OpenAPI\Client;
 
-use ArrayAccess;
 use DateTimeInterface;
 use DateTime;
 use GuzzleHttp\Psr7\Utils;
@@ -326,37 +325,6 @@ public static function toHeaderValue(string $value): string
         return self::toString($value);
     }
 
-    /**
-     * Take value and turn it into an array suitable for inclusion in
-     * the http body (form parameter). If it's a string, pass through unchanged
-     * If it's a datetime object, format it in ISO8601
-     *
-     * @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter
-     *
-     * @return array [key => value] of formdata
-     */
-    public static function toFormValue(
-        string $key,
-        string|bool|array|DateTime|ArrayAccess|\SplFileObject $value,
-    ): array {
-        if ($value instanceof \SplFileObject) {
-            return [$key => $value->getRealPath()];
-        } elseif (is_array($value) || $value instanceof ArrayAccess) {
-            $flattened = [];
-            $result = [];
-
-            self::flattenArray(json_decode(json_encode($value), true), $flattened);
-
-            foreach ($flattened as $k => $v) {
-                $result["{$key}{$k}"] = self::toString($v);
-            }
-
-            return $result;
-        } else {
-            return [$key => self::toString($value)];
-        }
-    }
-
     /**
      * Take value and turn it into a string suitable for inclusion in
      * the parameter. If it's a string, pass through unchanged
@@ -622,58 +590,4 @@ public static function buildQuery(array $params, $encoding = PHP_QUERY_RFC3986):
 
         return $qs ? (string) substr($qs, 0, -1) : '';
     }
-
-    /**
-     * Flattens an array of Model object and generates an array compatible
-     * with formdata - a single-level array where the keys use bracket
-     * notation to signify nested data.
-     *
-     * credit: https://github.com/FranBar1966/FlatPHP
-     */
-    private static function flattenArray(
-        ArrayAccess|array $source,
-        array &$destination,
-        string $start = '',
-    ) {
-        $opt = [
-            'prefix'          => '[',
-            'suffix'          => ']',
-            'suffix-end'      => true,
-            'prefix-list'     => '[',
-            'suffix-list'     => ']',
-            'suffix-list-end' => true,
-        ];
-
-        if (!is_array($source)) {
-            $source = (array) $source;
-        }
-
-        if (array_is_list($source)) {
-            $currentPrefix    = $opt['prefix-list'];
-            $currentSuffix    = $opt['suffix-list'];
-            $currentSuffixEnd = $opt['suffix-list-end'];
-        } else {
-            $currentPrefix    = $opt['prefix'];
-            $currentSuffix    = $opt['suffix'];
-            $currentSuffixEnd = $opt['suffix-end'];
-        }
-
-        $currentName = $start;
-
-        foreach ($source as $key => $val) {
-            $currentName .= $currentPrefix.$key;
-
-            if (is_array($val) && !empty($val)) {
-                $currentName .= "{$currentSuffix}";
-                self::flattenArray($val, $destination, $currentName);
-            } else {
-                if ($currentSuffixEnd) {
-                    $currentName .= $currentSuffix;
-                }
-                $destination[$currentName] = self::toString($val);
-            }
-
-            $currentName = $start;
-        }
-    }
 }
diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/AnotherFakeApi.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/AnotherFakeApi.php
index c69a68e49f40..449c06e5bab6 100644
--- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/AnotherFakeApi.php
+++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/AnotherFakeApi.php
@@ -38,6 +38,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/DefaultApi.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/DefaultApi.php
index aeff9f7c541a..1a3e901629ef 100644
--- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/DefaultApi.php
+++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/DefaultApi.php
@@ -38,6 +38,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/FakeApi.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/FakeApi.php
index 07ed43c9eaaf..efd61f135b88 100644
--- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/FakeApi.php
+++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/FakeApi.php
@@ -38,6 +38,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
@@ -4584,71 +4585,27 @@ public function testEndpointParametersRequest(
 
 
         // form params
-        if ($integer !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('integer', $integer));
-        }
-        // form params
-        if ($int32 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('int32', $int32));
-        }
-        // form params
-        if ($int64 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('int64', $int64));
-        }
-        // form params
-        if ($number !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('number', $number));
-        }
-        // form params
-        if ($float !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('float', $float));
-        }
-        // form params
-        if ($double !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('double', $double));
-        }
-        // form params
-        if ($string !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('string', $string));
-        }
-        // form params
-        if ($pattern_without_delimiter !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('pattern_without_delimiter', $pattern_without_delimiter));
-        }
-        // form params
-        if ($byte !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('byte', $byte));
-        }
-        // form params
-        if ($binary !== null) {
-            $multipart = true;
-            $formParams['binary'] = [];
-            $paramFiles = is_array($binary) ? $binary : [$binary];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['binary'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
-                    ? $paramFile
-                    : \GuzzleHttp\Psr7\Utils::tryFopen(
-                        ObjectSerializer::toFormValue('binary', $paramFile)['binary'],
-                        'rb'
-                    );
-            }
-        }
-        // form params
-        if ($date !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('date', $date));
-        }
-        // form params
-        if ($date_time !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('dateTime', $date_time));
-        }
-        // form params
-        if ($password !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('password', $password));
-        }
-        // form params
-        if ($callback !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('callback', $callback));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'integer' => $integer,
+            'int32' => $int32,
+            'int64' => $int64,
+            'number' => $number,
+            'float' => $float,
+            'double' => $double,
+            'string' => $string,
+            'pattern_without_delimiter' => $pattern_without_delimiter,
+            'byte' => $byte,
+            'binary' => $binary,
+            'date' => $date,
+            'date_time' => $date_time,
+            'password' => $password,
+            'callback' => $callback,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
@@ -5016,13 +4973,15 @@ public function testEnumParametersRequest(
 
 
         // form params
-        if ($enum_form_string_array !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('enum_form_string_array', $enum_form_string_array));
-        }
-        // form params
-        if ($enum_form_string !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('enum_form_string', $enum_form_string));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'enum_form_string_array' => $enum_form_string_array,
+            'enum_form_string' => $enum_form_string,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
@@ -6054,13 +6013,15 @@ public function testJsonFormDataRequest(
 
 
         // form params
-        if ($param !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('param', $param));
-        }
-        // form params
-        if ($param2 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('param2', $param2));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'param' => $param,
+            'param2' => $param2,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/FakeClassnameTags123Api.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/FakeClassnameTags123Api.php
index 2324371fccd7..77ad025812d6 100644
--- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/FakeClassnameTags123Api.php
+++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/FakeClassnameTags123Api.php
@@ -38,6 +38,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/PetApi.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/PetApi.php
index 564d43a7150e..f419abd77e04 100644
--- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/PetApi.php
+++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/PetApi.php
@@ -38,6 +38,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
@@ -2363,13 +2364,15 @@ public function updatePetWithFormRequest(
         }
 
         // form params
-        if ($name !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('name', $name));
-        }
-        // form params
-        if ($status !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('status', $status));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'name' => $name,
+            'status' => $status,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
@@ -2720,23 +2723,15 @@ public function uploadFileRequest(
         }
 
         // form params
-        if ($additional_metadata !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('additionalMetadata', $additional_metadata));
-        }
-        // form params
-        if ($file !== null) {
-            $multipart = true;
-            $formParams['file'] = [];
-            $paramFiles = is_array($file) ? $file : [$file];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['file'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
-                    ? $paramFile
-                    : \GuzzleHttp\Psr7\Utils::tryFopen(
-                        ObjectSerializer::toFormValue('file', $paramFile)['file'],
-                        'rb'
-                    );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'additional_metadata' => $additional_metadata,
+            'file' => $file,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['application/json', ],
@@ -3093,23 +3088,15 @@ public function uploadFileWithRequiredFileRequest(
         }
 
         // form params
-        if ($additional_metadata !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('additionalMetadata', $additional_metadata));
-        }
-        // form params
-        if ($required_file !== null) {
-            $multipart = true;
-            $formParams['requiredFile'] = [];
-            $paramFiles = is_array($required_file) ? $required_file : [$required_file];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['requiredFile'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
-                    ? $paramFile
-                    : \GuzzleHttp\Psr7\Utils::tryFopen(
-                        ObjectSerializer::toFormValue('requiredFile', $paramFile)['requiredFile'],
-                        'rb'
-                    );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'additional_metadata' => $additional_metadata,
+            'required_file' => $required_file,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['application/json', ],
diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/StoreApi.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/StoreApi.php
index 705302d06141..dae41b6fc425 100644
--- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/StoreApi.php
+++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/StoreApi.php
@@ -38,6 +38,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/UserApi.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/UserApi.php
index 97e46e3bf84d..d08d537c9828 100644
--- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/UserApi.php
+++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/Api/UserApi.php
@@ -38,6 +38,7 @@
 use OpenAPI\Client\ApiException;
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 
 /**
diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php
index a3f70ac9a987..87daefd327c2 100644
--- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php
+++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php
@@ -27,7 +27,6 @@
 
 namespace OpenAPI\Client;
 
-use ArrayAccess;
 use DateTimeInterface;
 use DateTime;
 use GuzzleHttp\Psr7\Utils;
@@ -325,37 +324,6 @@ public static function toHeaderValue(string $value): string
         return self::toString($value);
     }
 
-    /**
-     * Take value and turn it into an array suitable for inclusion in
-     * the http body (form parameter). If it's a string, pass through unchanged
-     * If it's a datetime object, format it in ISO8601
-     *
-     * @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter
-     *
-     * @return array [key => value] of formdata
-     */
-    public static function toFormValue(
-        string $key,
-        string|bool|array|DateTime|ArrayAccess|\SplFileObject $value,
-    ): array {
-        if ($value instanceof \SplFileObject) {
-            return [$key => $value->getRealPath()];
-        } elseif (is_array($value) || $value instanceof ArrayAccess) {
-            $flattened = [];
-            $result = [];
-
-            self::flattenArray(json_decode(json_encode($value), true), $flattened);
-
-            foreach ($flattened as $k => $v) {
-                $result["{$key}{$k}"] = self::toString($v);
-            }
-
-            return $result;
-        } else {
-            return [$key => self::toString($value)];
-        }
-    }
-
     /**
      * Take value and turn it into a string suitable for inclusion in
      * the parameter. If it's a string, pass through unchanged
@@ -621,58 +589,4 @@ public static function buildQuery(array $params, $encoding = PHP_QUERY_RFC3986):
 
         return $qs ? (string) substr($qs, 0, -1) : '';
     }
-
-    /**
-     * Flattens an array of Model object and generates an array compatible
-     * with formdata - a single-level array where the keys use bracket
-     * notation to signify nested data.
-     *
-     * credit: https://github.com/FranBar1966/FlatPHP
-     */
-    private static function flattenArray(
-        ArrayAccess|array $source,
-        array &$destination,
-        string $start = '',
-    ) {
-        $opt = [
-            'prefix'          => '[',
-            'suffix'          => ']',
-            'suffix-end'      => true,
-            'prefix-list'     => '[',
-            'suffix-list'     => ']',
-            'suffix-list-end' => true,
-        ];
-
-        if (!is_array($source)) {
-            $source = (array) $source;
-        }
-
-        if (array_is_list($source)) {
-            $currentPrefix    = $opt['prefix-list'];
-            $currentSuffix    = $opt['suffix-list'];
-            $currentSuffixEnd = $opt['suffix-list-end'];
-        } else {
-            $currentPrefix    = $opt['prefix'];
-            $currentSuffix    = $opt['suffix'];
-            $currentSuffixEnd = $opt['suffix-end'];
-        }
-
-        $currentName = $start;
-
-        foreach ($source as $key => $val) {
-            $currentName .= $currentPrefix.$key;
-
-            if (is_array($val) && !empty($val)) {
-                $currentName .= "{$currentSuffix}";
-                self::flattenArray($val, $destination, $currentName);
-            } else {
-                if ($currentSuffixEnd) {
-                    $currentName .= $currentSuffix;
-                }
-                $destination[$currentName] = self::toString($val);
-            }
-
-            $currentName = $start;
-        }
-    }
 }
diff --git a/samples/client/petstore/php/psr-18/lib/Api/AnotherFakeApi.php b/samples/client/petstore/php/psr-18/lib/Api/AnotherFakeApi.php
index 7e5e1afbfc2c..af63b05a491f 100644
--- a/samples/client/petstore/php/psr-18/lib/Api/AnotherFakeApi.php
+++ b/samples/client/petstore/php/psr-18/lib/Api/AnotherFakeApi.php
@@ -43,6 +43,7 @@
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\DebugPlugin;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Client\ClientInterface;
diff --git a/samples/client/petstore/php/psr-18/lib/Api/DefaultApi.php b/samples/client/petstore/php/psr-18/lib/Api/DefaultApi.php
index 005d6ffbafed..11fbdc8d0c4b 100644
--- a/samples/client/petstore/php/psr-18/lib/Api/DefaultApi.php
+++ b/samples/client/petstore/php/psr-18/lib/Api/DefaultApi.php
@@ -43,6 +43,7 @@
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\DebugPlugin;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Client\ClientInterface;
diff --git a/samples/client/petstore/php/psr-18/lib/Api/FakeApi.php b/samples/client/petstore/php/psr-18/lib/Api/FakeApi.php
index 360a3b16f4c8..2500330fb6e6 100644
--- a/samples/client/petstore/php/psr-18/lib/Api/FakeApi.php
+++ b/samples/client/petstore/php/psr-18/lib/Api/FakeApi.php
@@ -43,6 +43,7 @@
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\DebugPlugin;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Client\ClientInterface;
@@ -4001,69 +4002,27 @@ public function testEndpointParametersRequest($number, $double, $pattern_without
 
 
         // form params
-        if ($integer !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('integer', $integer));
-        }
-        // form params
-        if ($int32 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('int32', $int32));
-        }
-        // form params
-        if ($int64 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('int64', $int64));
-        }
-        // form params
-        if ($number !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('number', $number));
-        }
-        // form params
-        if ($float !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('float', $float));
-        }
-        // form params
-        if ($double !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('double', $double));
-        }
-        // form params
-        if ($string !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('string', $string));
-        }
-        // form params
-        if ($pattern_without_delimiter !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('pattern_without_delimiter', $pattern_without_delimiter));
-        }
-        // form params
-        if ($byte !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('byte', $byte));
-        }
-        // form params
-        if ($binary !== null) {
-            $multipart = true;
-            $formParams['binary'] = [];
-            $paramFiles = is_array($binary) ? $binary : [$binary];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['binary'][] = \GuzzleHttp\Psr7\try_fopen(
-                    ObjectSerializer::toFormValue('binary', $paramFile)['binary'],
-                    'rb'
-                );
-            }
-        }
-        // form params
-        if ($date !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('date', $date));
-        }
-        // form params
-        if ($date_time !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('dateTime', $date_time));
-        }
-        // form params
-        if ($password !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('password', $password));
-        }
-        // form params
-        if ($callback !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('callback', $callback));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'integer' => $integer,
+            'int32' => $int32,
+            'int64' => $int64,
+            'number' => $number,
+            'float' => $float,
+            'double' => $double,
+            'string' => $string,
+            'pattern_without_delimiter' => $pattern_without_delimiter,
+            'byte' => $byte,
+            'binary' => $binary,
+            'date' => $date,
+            'date_time' => $date_time,
+            'password' => $password,
+            'callback' => $callback,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
@@ -4370,13 +4329,15 @@ public function testEnumParametersRequest($enum_header_string_array = null, $enu
 
 
         // form params
-        if ($enum_form_string_array !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('enum_form_string_array', $enum_form_string_array));
-        }
-        // form params
-        if ($enum_form_string !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('enum_form_string', $enum_form_string));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'enum_form_string_array' => $enum_form_string_array,
+            'enum_form_string' => $enum_form_string,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
@@ -5316,13 +5277,15 @@ public function testJsonFormDataRequest($param, $param2)
 
 
         // form params
-        if ($param !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('param', $param));
-        }
-        // form params
-        if ($param2 !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('param2', $param2));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'param' => $param,
+            'param2' => $param2,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
diff --git a/samples/client/petstore/php/psr-18/lib/Api/FakeClassnameTags123Api.php b/samples/client/petstore/php/psr-18/lib/Api/FakeClassnameTags123Api.php
index a0acbf6a5326..275def167e16 100644
--- a/samples/client/petstore/php/psr-18/lib/Api/FakeClassnameTags123Api.php
+++ b/samples/client/petstore/php/psr-18/lib/Api/FakeClassnameTags123Api.php
@@ -43,6 +43,7 @@
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\DebugPlugin;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Client\ClientInterface;
diff --git a/samples/client/petstore/php/psr-18/lib/Api/PetApi.php b/samples/client/petstore/php/psr-18/lib/Api/PetApi.php
index 29b35822dba0..68514f74315e 100644
--- a/samples/client/petstore/php/psr-18/lib/Api/PetApi.php
+++ b/samples/client/petstore/php/psr-18/lib/Api/PetApi.php
@@ -43,6 +43,7 @@
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\DebugPlugin;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Client\ClientInterface;
@@ -1817,13 +1818,15 @@ public function updatePetWithFormRequest($pet_id, $name = null, $status = null)
         }
 
         // form params
-        if ($name !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('name', $name));
-        }
-        // form params
-        if ($status !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('status', $status));
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'name' => $name,
+            'status' => $status,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             [],
@@ -2093,21 +2096,15 @@ public function uploadFileRequest($pet_id, $additional_metadata = null, $file =
         }
 
         // form params
-        if ($additional_metadata !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('additionalMetadata', $additional_metadata));
-        }
-        // form params
-        if ($file !== null) {
-            $multipart = true;
-            $formParams['file'] = [];
-            $paramFiles = is_array($file) ? $file : [$file];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['file'][] = \GuzzleHttp\Psr7\try_fopen(
-                    ObjectSerializer::toFormValue('file', $paramFile)['file'],
-                    'rb'
-                );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'additional_metadata' => $additional_metadata,
+            'file' => $file,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['application/json'],
@@ -2383,21 +2380,15 @@ public function uploadFileWithRequiredFileRequest($pet_id, $required_file, $addi
         }
 
         // form params
-        if ($additional_metadata !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('additionalMetadata', $additional_metadata));
-        }
-        // form params
-        if ($required_file !== null) {
-            $multipart = true;
-            $formParams['requiredFile'] = [];
-            $paramFiles = is_array($required_file) ? $required_file : [$required_file];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['requiredFile'][] = \GuzzleHttp\Psr7\try_fopen(
-                    ObjectSerializer::toFormValue('requiredFile', $paramFile)['requiredFile'],
-                    'rb'
-                );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'additional_metadata' => $additional_metadata,
+            'required_file' => $required_file,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['application/json'],
@@ -2710,53 +2701,21 @@ public function uploadImageFullFormDataRequest($pet_id, $name, $photo_urls, $id
         }
 
         // form params
-        if ($id !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('id', $id));
-        }
-        // form params
-        if ($category !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('category', $category));
-        }
-        // form params
-        if ($name !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('name', $name));
-        }
-        // form params
-        if ($photo_urls !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('photoUrls', $photo_urls));
-        }
-        // form params
-        if ($tags !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('tags', $tags));
-        }
-        // form params
-        if ($status !== null) {
-            $formParams = array_merge($formParams, ObjectSerializer::toFormValue('status', $status));
-        }
-        // form params
-        if ($file !== null) {
-            $multipart = true;
-            $formParams['file'] = [];
-            $paramFiles = is_array($file) ? $file : [$file];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['file'][] = \GuzzleHttp\Psr7\try_fopen(
-                    ObjectSerializer::toFormValue('file', $paramFile)['file'],
-                    'rb'
-                );
-            }
-        }
-        // form params
-        if ($multiple_files !== null) {
-            $multipart = true;
-            $formParams['multiple_files'] = [];
-            $paramFiles = is_array($multiple_files) ? $multiple_files : [$multiple_files];
-            foreach ($paramFiles as $paramFile) {
-                $formParams['multiple_files'][] = \GuzzleHttp\Psr7\try_fopen(
-                    ObjectSerializer::toFormValue('multiple_files', $paramFile)['multiple_files'],
-                    'rb'
-                );
-            }
-        }
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'id' => $id,
+            'category' => $category,
+            'name' => $name,
+            'photo_urls' => $photo_urls,
+            'tags' => $tags,
+            'status' => $status,
+            'file' => $file,
+            'multiple_files' => $multiple_files,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
 
         $headers = $this->headerSelector->selectHeaders(
             ['application/json'],
diff --git a/samples/client/petstore/php/psr-18/lib/Api/StoreApi.php b/samples/client/petstore/php/psr-18/lib/Api/StoreApi.php
index 9f41d0568f2d..7d49d82294b1 100644
--- a/samples/client/petstore/php/psr-18/lib/Api/StoreApi.php
+++ b/samples/client/petstore/php/psr-18/lib/Api/StoreApi.php
@@ -43,6 +43,7 @@
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\DebugPlugin;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Client\ClientInterface;
diff --git a/samples/client/petstore/php/psr-18/lib/Api/UserApi.php b/samples/client/petstore/php/psr-18/lib/Api/UserApi.php
index 7e001b952306..3179c9fa3640 100644
--- a/samples/client/petstore/php/psr-18/lib/Api/UserApi.php
+++ b/samples/client/petstore/php/psr-18/lib/Api/UserApi.php
@@ -43,6 +43,7 @@
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\DebugPlugin;
 use OpenAPI\Client\HeaderSelector;
+use OpenAPI\Client\FormDataProcessor;
 use OpenAPI\Client\ObjectSerializer;
 use Psr\Http\Client\ClientExceptionInterface;
 use Psr\Http\Client\ClientInterface;

From d886bdc05b053ad0cf71cada48f793070d9c5566 Mon Sep 17 00:00:00 2001
From: Juan Treminio <jtreminio@gmail.com>
Date: Fri, 28 Mar 2025 18:51:44 -0500
Subject: [PATCH 04/10] Adds tests

---
 ...ith-fake-endpoints-models-for-testing.yaml |  34 +
 .../.openapi-generator/FILES                  |   1 +
 .../src/FormDataProcessor.php                 | 237 ++++++
 .../php-nextgen/.openapi-generator/FILES      |   1 +
 .../php-nextgen/src/FormDataProcessor.php     | 237 ++++++
 .../.openapi-generator/FILES                  |   1 +
 .../src/FormDataProcessor.php                 | 236 ++++++
 .../.openapi-generator/FILES                  |   4 +
 .../petstore/php/OpenAPIClient-php/README.md  |   2 +
 .../php/OpenAPIClient-php/docs/Api/PetApi.md  |  63 ++
 .../docs/Model/PetWithFile.md                 |  16 +
 .../php/OpenAPIClient-php/lib/Api/PetApi.php  | 331 +++++++++
 .../lib/FormDataProcessor.php                 | 242 ++++++
 .../lib/Model/PetWithFile.php                 | 691 ++++++++++++++++++
 .../test/Model/PetWithFileTest.php            | 153 ++++
 .../tests/FormDataProcessorTest.php           | 172 +++++
 .../tests/ObjectSerializerTest.php            |  94 ---
 .../php/psr-18/.openapi-generator/FILES       |   4 +
 samples/client/petstore/php/psr-18/README.md  |   2 +
 .../petstore/php/psr-18/docs/Api/PetApi.md    |  63 ++
 .../php/psr-18/docs/Model/PetWithFile.md      |  16 +
 .../petstore/php/psr-18/lib/Api/PetApi.php    | 272 +++++++
 .../php/psr-18/lib/FormDataProcessor.php      | 242 ++++++
 .../php/psr-18/lib/Model/PetWithFile.php      | 691 ++++++++++++++++++
 .../php/psr-18/test/Model/PetWithFileTest.php | 153 ++++
 25 files changed, 3864 insertions(+), 94 deletions(-)
 create mode 100644 samples/client/echo_api/php-nextgen-streaming/src/FormDataProcessor.php
 create mode 100644 samples/client/echo_api/php-nextgen/src/FormDataProcessor.php
 create mode 100644 samples/client/petstore/php-nextgen/OpenAPIClient-php/src/FormDataProcessor.php
 create mode 100644 samples/client/petstore/php/OpenAPIClient-php/docs/Model/PetWithFile.md
 create mode 100644 samples/client/petstore/php/OpenAPIClient-php/lib/FormDataProcessor.php
 create mode 100644 samples/client/petstore/php/OpenAPIClient-php/lib/Model/PetWithFile.php
 create mode 100644 samples/client/petstore/php/OpenAPIClient-php/test/Model/PetWithFileTest.php
 create mode 100644 samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php
 create mode 100644 samples/client/petstore/php/psr-18/docs/Model/PetWithFile.md
 create mode 100644 samples/client/petstore/php/psr-18/lib/FormDataProcessor.php
 create mode 100644 samples/client/petstore/php/psr-18/lib/Model/PetWithFile.php
 create mode 100644 samples/client/petstore/php/psr-18/test/Model/PetWithFileTest.php

diff --git a/modules/openapi-generator/src/test/resources/3_0/php/petstore-with-fake-endpoints-models-for-testing.yaml b/modules/openapi-generator/src/test/resources/3_0/php/petstore-with-fake-endpoints-models-for-testing.yaml
index e971a99dc261..d3492997de57 100644
--- a/modules/openapi-generator/src/test/resources/3_0/php/petstore-with-fake-endpoints-models-for-testing.yaml
+++ b/modules/openapi-generator/src/test/resources/3_0/php/petstore-with-fake-endpoints-models-for-testing.yaml
@@ -342,6 +342,40 @@ paths:
           multipart/form-data:
             schema:
               $ref: '#/components/schemas/PetWithFile'
+  '/pet/{petId}/uploadImageFullFormDataNested':
+    post:
+      tags:
+        - pet
+      summary: uploads an image attached to a Pet object as formdata
+      description: ''
+      operationId: uploadImageFullFormDataNested
+      parameters:
+        - name: petId
+          in: path
+          description: ID of pet to update
+          required: true
+          schema:
+            type: integer
+            format: int64
+      responses:
+        '200':
+          description: successful operation
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ApiResponse'
+      security:
+        - petstore_auth:
+            - 'write:pets'
+            - 'read:pets'
+      requestBody:
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              properties:
+                pet:
+                  $ref: '#/components/schemas/PetWithFile'
   /store/inventory:
     get:
       tags:
diff --git a/samples/client/echo_api/php-nextgen-streaming/.openapi-generator/FILES b/samples/client/echo_api/php-nextgen-streaming/.openapi-generator/FILES
index c51141c743e7..d849d4cffa36 100644
--- a/samples/client/echo_api/php-nextgen-streaming/.openapi-generator/FILES
+++ b/samples/client/echo_api/php-nextgen-streaming/.openapi-generator/FILES
@@ -32,6 +32,7 @@ src/Api/PathApi.php
 src/Api/QueryApi.php
 src/ApiException.php
 src/Configuration.php
+src/FormDataProcessor.php
 src/HeaderSelector.php
 src/Model/Bird.php
 src/Model/Category.php
diff --git a/samples/client/echo_api/php-nextgen-streaming/src/FormDataProcessor.php b/samples/client/echo_api/php-nextgen-streaming/src/FormDataProcessor.php
new file mode 100644
index 000000000000..b7a31b3f2a01
--- /dev/null
+++ b/samples/client/echo_api/php-nextgen-streaming/src/FormDataProcessor.php
@@ -0,0 +1,237 @@
+<?php
+/**
+ * FormDataProcessor
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * Echo Server API
+ *
+ * Echo Server API
+ *
+ * The version of the OpenAPI document: 0.1.0
+ * Contact: team@openapitools.org
+ * @generated Generated by: https://openapi-generator.tech
+ * Generator version: 7.13.0-SNAPSHOT
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace OpenAPI\Client;
+
+use ArrayAccess;
+use DateTime;
+use GuzzleHttp\Psr7\Utils;
+use Psr\Http\Message\StreamInterface;
+use SplFileObject;
+use OpenAPI\Client\Model\ModelInterface;
+
+class FormDataProcessor
+{
+    /**
+     * Tags whether payload passed to ::prepare() contains one or more
+     * SplFileObject or stream values.
+     */
+    public bool $has_file = false;
+
+    /**
+     * Take value and turn it into an array suitable for inclusion in
+     * the http body (form parameter). If it's a string, pass through unchanged
+     * If it's a datetime object, format it in ISO8601
+     *
+     * @param array<string|bool|array|DateTime|ArrayAccess|SplFileObject> $values the value of the form parameter
+     *
+     * @return array [key => value] of formdata
+     */
+    public function prepare(array $values): array
+    {
+        $this->has_file = false;
+        $result = [];
+
+        foreach ($values as $k => $v) {
+            if ($v === null) {
+                continue;
+            }
+
+            $result[$k] = $this->makeFormSafe($v);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Flattens a multi-level array of data and generates a single-level array
+     * compatible with formdata - a single-level array where the keys use bracket
+     * notation to signify nested data.
+     *
+     * credit: https://github.com/FranBar1966/FlatPHP
+     */
+    public static function flatten(array $source, string $start = ''): array
+    {
+        $opt = [
+            'prefix'          => '[',
+            'suffix'          => ']',
+            'suffix-end'      => true,
+            'prefix-list'     => '[',
+            'suffix-list'     => ']',
+            'suffix-list-end' => true,
+        ];
+
+        if ($start === '') {
+            $currentPrefix = '';
+            $currentSuffix = '';
+            $currentSuffixEnd = false;
+        } elseif (array_is_list($source)) {
+            $currentPrefix = $opt['prefix-list'];
+            $currentSuffix = $opt['suffix-list'];
+            $currentSuffixEnd = $opt['suffix-list-end'];
+        } else {
+            $currentPrefix = $opt['prefix'];
+            $currentSuffix = $opt['suffix'];
+            $currentSuffixEnd = $opt['suffix-end'];
+        }
+
+        $currentName = $start;
+        $result = [];
+
+        foreach ($source as $key => $val) {
+            $currentName .= $currentPrefix . $key;
+
+            if (is_array($val) && !empty($val)) {
+                $currentName .= $currentSuffix;
+                $result += self::flatten($val, $currentName);
+            } else {
+                if ($currentSuffixEnd) {
+                    $currentName .= $currentSuffix;
+                }
+
+                $result[$currentName] = ObjectSerializer::toString($val);
+            }
+
+            $currentName = $start;
+        }
+
+        return $result;
+    }
+
+    /**
+     * formdata must be limited to scalars or arrays of scalar values,
+     * or a resource for a file upload. Here we iterate through all available
+     * data and identify how to handle each scenario
+     *
+     * @param string|bool|array|DateTime|ArrayAccess|SplFileObject $value
+     */
+    protected function makeFormSafe(mixed $value)
+    {
+        if ($value instanceof SplFileObject) {
+            return $this->processFiles([$value])[0];
+        }
+
+        if (is_resource($value)) {
+            $this->has_file = true;
+
+            return $value;
+        }
+
+        if ($value instanceof ModelInterface) {
+            return $this->processModel($value);
+        }
+
+        if (is_array($value) || is_object($value)) {
+            $data = [];
+
+            foreach ($value as $k => $v) {
+                $data[$k] = $this->makeFormSafe($v);
+            }
+
+            return $data;
+        }
+
+        return ObjectSerializer::toString($value);
+    }
+
+    /**
+     * We are able to handle nested ModelInterface. We do not simply call
+     * json_decode(json_encode()) because any given model may have binary data
+     * or other data that cannot be serialized to a JSON string
+     */
+    protected function processModel(ModelInterface $model): array
+    {
+        $result = [];
+
+        foreach ($model::openAPITypes() as $name => $type) {
+            $value = $model->offsetGet($name);
+
+            if ($value === null) {
+                continue;
+            }
+
+            if (str_contains($type, '\SplFileObject')) {
+                $file = is_array($value) ? $value : [$value];
+                $result[$name] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($value instanceof ModelInterface) {
+                $result[$name] = $this->processModel($value);
+
+                continue;
+            }
+
+            if (is_array($value) || is_object($value)) {
+                $result[$name] = $this->makeFormSafe($value);
+
+                continue;
+            }
+
+            $result[$name] = ObjectSerializer::toString($value);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Handle file data
+     */
+    protected function processFiles(array $files): array
+    {
+        $this->has_file = true;
+
+        $result = [];
+
+        foreach ($files as $i => $file) {
+            if (is_array($file)) {
+                $result[$i] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($file instanceof StreamInterface) {
+                $result[$i] = $file;
+
+                continue;
+            }
+
+            if ($file instanceof SplFileObject) {
+                $result[$i] = $this->tryFopen($file);
+            }
+        }
+
+        return $result;
+    }
+
+    private function tryFopen(SplFileObject $file)
+    {
+        return Utils::tryFopen($file->getRealPath(), 'rb');
+    }
+}
diff --git a/samples/client/echo_api/php-nextgen/.openapi-generator/FILES b/samples/client/echo_api/php-nextgen/.openapi-generator/FILES
index c51141c743e7..d849d4cffa36 100644
--- a/samples/client/echo_api/php-nextgen/.openapi-generator/FILES
+++ b/samples/client/echo_api/php-nextgen/.openapi-generator/FILES
@@ -32,6 +32,7 @@ src/Api/PathApi.php
 src/Api/QueryApi.php
 src/ApiException.php
 src/Configuration.php
+src/FormDataProcessor.php
 src/HeaderSelector.php
 src/Model/Bird.php
 src/Model/Category.php
diff --git a/samples/client/echo_api/php-nextgen/src/FormDataProcessor.php b/samples/client/echo_api/php-nextgen/src/FormDataProcessor.php
new file mode 100644
index 000000000000..b7a31b3f2a01
--- /dev/null
+++ b/samples/client/echo_api/php-nextgen/src/FormDataProcessor.php
@@ -0,0 +1,237 @@
+<?php
+/**
+ * FormDataProcessor
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * Echo Server API
+ *
+ * Echo Server API
+ *
+ * The version of the OpenAPI document: 0.1.0
+ * Contact: team@openapitools.org
+ * @generated Generated by: https://openapi-generator.tech
+ * Generator version: 7.13.0-SNAPSHOT
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace OpenAPI\Client;
+
+use ArrayAccess;
+use DateTime;
+use GuzzleHttp\Psr7\Utils;
+use Psr\Http\Message\StreamInterface;
+use SplFileObject;
+use OpenAPI\Client\Model\ModelInterface;
+
+class FormDataProcessor
+{
+    /**
+     * Tags whether payload passed to ::prepare() contains one or more
+     * SplFileObject or stream values.
+     */
+    public bool $has_file = false;
+
+    /**
+     * Take value and turn it into an array suitable for inclusion in
+     * the http body (form parameter). If it's a string, pass through unchanged
+     * If it's a datetime object, format it in ISO8601
+     *
+     * @param array<string|bool|array|DateTime|ArrayAccess|SplFileObject> $values the value of the form parameter
+     *
+     * @return array [key => value] of formdata
+     */
+    public function prepare(array $values): array
+    {
+        $this->has_file = false;
+        $result = [];
+
+        foreach ($values as $k => $v) {
+            if ($v === null) {
+                continue;
+            }
+
+            $result[$k] = $this->makeFormSafe($v);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Flattens a multi-level array of data and generates a single-level array
+     * compatible with formdata - a single-level array where the keys use bracket
+     * notation to signify nested data.
+     *
+     * credit: https://github.com/FranBar1966/FlatPHP
+     */
+    public static function flatten(array $source, string $start = ''): array
+    {
+        $opt = [
+            'prefix'          => '[',
+            'suffix'          => ']',
+            'suffix-end'      => true,
+            'prefix-list'     => '[',
+            'suffix-list'     => ']',
+            'suffix-list-end' => true,
+        ];
+
+        if ($start === '') {
+            $currentPrefix = '';
+            $currentSuffix = '';
+            $currentSuffixEnd = false;
+        } elseif (array_is_list($source)) {
+            $currentPrefix = $opt['prefix-list'];
+            $currentSuffix = $opt['suffix-list'];
+            $currentSuffixEnd = $opt['suffix-list-end'];
+        } else {
+            $currentPrefix = $opt['prefix'];
+            $currentSuffix = $opt['suffix'];
+            $currentSuffixEnd = $opt['suffix-end'];
+        }
+
+        $currentName = $start;
+        $result = [];
+
+        foreach ($source as $key => $val) {
+            $currentName .= $currentPrefix . $key;
+
+            if (is_array($val) && !empty($val)) {
+                $currentName .= $currentSuffix;
+                $result += self::flatten($val, $currentName);
+            } else {
+                if ($currentSuffixEnd) {
+                    $currentName .= $currentSuffix;
+                }
+
+                $result[$currentName] = ObjectSerializer::toString($val);
+            }
+
+            $currentName = $start;
+        }
+
+        return $result;
+    }
+
+    /**
+     * formdata must be limited to scalars or arrays of scalar values,
+     * or a resource for a file upload. Here we iterate through all available
+     * data and identify how to handle each scenario
+     *
+     * @param string|bool|array|DateTime|ArrayAccess|SplFileObject $value
+     */
+    protected function makeFormSafe(mixed $value)
+    {
+        if ($value instanceof SplFileObject) {
+            return $this->processFiles([$value])[0];
+        }
+
+        if (is_resource($value)) {
+            $this->has_file = true;
+
+            return $value;
+        }
+
+        if ($value instanceof ModelInterface) {
+            return $this->processModel($value);
+        }
+
+        if (is_array($value) || is_object($value)) {
+            $data = [];
+
+            foreach ($value as $k => $v) {
+                $data[$k] = $this->makeFormSafe($v);
+            }
+
+            return $data;
+        }
+
+        return ObjectSerializer::toString($value);
+    }
+
+    /**
+     * We are able to handle nested ModelInterface. We do not simply call
+     * json_decode(json_encode()) because any given model may have binary data
+     * or other data that cannot be serialized to a JSON string
+     */
+    protected function processModel(ModelInterface $model): array
+    {
+        $result = [];
+
+        foreach ($model::openAPITypes() as $name => $type) {
+            $value = $model->offsetGet($name);
+
+            if ($value === null) {
+                continue;
+            }
+
+            if (str_contains($type, '\SplFileObject')) {
+                $file = is_array($value) ? $value : [$value];
+                $result[$name] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($value instanceof ModelInterface) {
+                $result[$name] = $this->processModel($value);
+
+                continue;
+            }
+
+            if (is_array($value) || is_object($value)) {
+                $result[$name] = $this->makeFormSafe($value);
+
+                continue;
+            }
+
+            $result[$name] = ObjectSerializer::toString($value);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Handle file data
+     */
+    protected function processFiles(array $files): array
+    {
+        $this->has_file = true;
+
+        $result = [];
+
+        foreach ($files as $i => $file) {
+            if (is_array($file)) {
+                $result[$i] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($file instanceof StreamInterface) {
+                $result[$i] = $file;
+
+                continue;
+            }
+
+            if ($file instanceof SplFileObject) {
+                $result[$i] = $this->tryFopen($file);
+            }
+        }
+
+        return $result;
+    }
+
+    private function tryFopen(SplFileObject $file)
+    {
+        return Utils::tryFopen($file->getRealPath(), 'rb');
+    }
+}
diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/.openapi-generator/FILES b/samples/client/petstore/php-nextgen/OpenAPIClient-php/.openapi-generator/FILES
index ebd39c9d9de9..2f93e8839383 100644
--- a/samples/client/petstore/php-nextgen/OpenAPIClient-php/.openapi-generator/FILES
+++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/.openapi-generator/FILES
@@ -73,6 +73,7 @@ src/Api/StoreApi.php
 src/Api/UserApi.php
 src/ApiException.php
 src/Configuration.php
+src/FormDataProcessor.php
 src/HeaderSelector.php
 src/Model/AdditionalPropertiesClass.php
 src/Model/AllOfWithSingleRef.php
diff --git a/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/FormDataProcessor.php b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/FormDataProcessor.php
new file mode 100644
index 000000000000..f044f1c6dbc7
--- /dev/null
+++ b/samples/client/petstore/php-nextgen/OpenAPIClient-php/src/FormDataProcessor.php
@@ -0,0 +1,236 @@
+<?php
+/**
+ * FormDataProcessor
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * OpenAPI Petstore
+ *
+ * This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * @generated Generated by: https://openapi-generator.tech
+ * Generator version: 7.13.0-SNAPSHOT
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace OpenAPI\Client;
+
+use ArrayAccess;
+use DateTime;
+use GuzzleHttp\Psr7\Utils;
+use Psr\Http\Message\StreamInterface;
+use SplFileObject;
+use OpenAPI\Client\Model\ModelInterface;
+
+class FormDataProcessor
+{
+    /**
+     * Tags whether payload passed to ::prepare() contains one or more
+     * SplFileObject or stream values.
+     */
+    public bool $has_file = false;
+
+    /**
+     * Take value and turn it into an array suitable for inclusion in
+     * the http body (form parameter). If it's a string, pass through unchanged
+     * If it's a datetime object, format it in ISO8601
+     *
+     * @param array<string|bool|array|DateTime|ArrayAccess|SplFileObject> $values the value of the form parameter
+     *
+     * @return array [key => value] of formdata
+     */
+    public function prepare(array $values): array
+    {
+        $this->has_file = false;
+        $result = [];
+
+        foreach ($values as $k => $v) {
+            if ($v === null) {
+                continue;
+            }
+
+            $result[$k] = $this->makeFormSafe($v);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Flattens a multi-level array of data and generates a single-level array
+     * compatible with formdata - a single-level array where the keys use bracket
+     * notation to signify nested data.
+     *
+     * credit: https://github.com/FranBar1966/FlatPHP
+     */
+    public static function flatten(array $source, string $start = ''): array
+    {
+        $opt = [
+            'prefix'          => '[',
+            'suffix'          => ']',
+            'suffix-end'      => true,
+            'prefix-list'     => '[',
+            'suffix-list'     => ']',
+            'suffix-list-end' => true,
+        ];
+
+        if ($start === '') {
+            $currentPrefix = '';
+            $currentSuffix = '';
+            $currentSuffixEnd = false;
+        } elseif (array_is_list($source)) {
+            $currentPrefix = $opt['prefix-list'];
+            $currentSuffix = $opt['suffix-list'];
+            $currentSuffixEnd = $opt['suffix-list-end'];
+        } else {
+            $currentPrefix = $opt['prefix'];
+            $currentSuffix = $opt['suffix'];
+            $currentSuffixEnd = $opt['suffix-end'];
+        }
+
+        $currentName = $start;
+        $result = [];
+
+        foreach ($source as $key => $val) {
+            $currentName .= $currentPrefix . $key;
+
+            if (is_array($val) && !empty($val)) {
+                $currentName .= $currentSuffix;
+                $result += self::flatten($val, $currentName);
+            } else {
+                if ($currentSuffixEnd) {
+                    $currentName .= $currentSuffix;
+                }
+
+                $result[$currentName] = ObjectSerializer::toString($val);
+            }
+
+            $currentName = $start;
+        }
+
+        return $result;
+    }
+
+    /**
+     * formdata must be limited to scalars or arrays of scalar values,
+     * or a resource for a file upload. Here we iterate through all available
+     * data and identify how to handle each scenario
+     *
+     * @param string|bool|array|DateTime|ArrayAccess|SplFileObject $value
+     */
+    protected function makeFormSafe(mixed $value)
+    {
+        if ($value instanceof SplFileObject) {
+            return $this->processFiles([$value])[0];
+        }
+
+        if (is_resource($value)) {
+            $this->has_file = true;
+
+            return $value;
+        }
+
+        if ($value instanceof ModelInterface) {
+            return $this->processModel($value);
+        }
+
+        if (is_array($value) || is_object($value)) {
+            $data = [];
+
+            foreach ($value as $k => $v) {
+                $data[$k] = $this->makeFormSafe($v);
+            }
+
+            return $data;
+        }
+
+        return ObjectSerializer::toString($value);
+    }
+
+    /**
+     * We are able to handle nested ModelInterface. We do not simply call
+     * json_decode(json_encode()) because any given model may have binary data
+     * or other data that cannot be serialized to a JSON string
+     */
+    protected function processModel(ModelInterface $model): array
+    {
+        $result = [];
+
+        foreach ($model::openAPITypes() as $name => $type) {
+            $value = $model->offsetGet($name);
+
+            if ($value === null) {
+                continue;
+            }
+
+            if (str_contains($type, '\SplFileObject')) {
+                $file = is_array($value) ? $value : [$value];
+                $result[$name] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($value instanceof ModelInterface) {
+                $result[$name] = $this->processModel($value);
+
+                continue;
+            }
+
+            if (is_array($value) || is_object($value)) {
+                $result[$name] = $this->makeFormSafe($value);
+
+                continue;
+            }
+
+            $result[$name] = ObjectSerializer::toString($value);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Handle file data
+     */
+    protected function processFiles(array $files): array
+    {
+        $this->has_file = true;
+
+        $result = [];
+
+        foreach ($files as $i => $file) {
+            if (is_array($file)) {
+                $result[$i] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($file instanceof StreamInterface) {
+                $result[$i] = $file;
+
+                continue;
+            }
+
+            if ($file instanceof SplFileObject) {
+                $result[$i] = $this->tryFopen($file);
+            }
+        }
+
+        return $result;
+    }
+
+    private function tryFopen(SplFileObject $file)
+    {
+        return Utils::tryFopen($file->getRealPath(), 'rb');
+    }
+}
diff --git a/samples/client/petstore/php/OpenAPIClient-php/.openapi-generator/FILES b/samples/client/petstore/php/OpenAPIClient-php/.openapi-generator/FILES
index 163723370d10..b8f153bf7f98 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/.openapi-generator/FILES
+++ b/samples/client/petstore/php/OpenAPIClient-php/.openapi-generator/FILES
@@ -53,6 +53,7 @@ docs/Model/OuterEnumInteger.md
 docs/Model/OuterEnumIntegerDefaultValue.md
 docs/Model/OuterObjectWithEnumProperty.md
 docs/Model/Pet.md
+docs/Model/PetWithFile.md
 docs/Model/PropertyNameMapping.md
 docs/Model/ReadOnlyFirst.md
 docs/Model/SingleRefType.md
@@ -70,6 +71,7 @@ lib/Api/StoreApi.php
 lib/Api/UserApi.php
 lib/ApiException.php
 lib/Configuration.php
+lib/FormDataProcessor.php
 lib/HeaderSelector.php
 lib/Model/AdditionalPropertiesClass.php
 lib/Model/AllOfWithSingleRef.php
@@ -115,6 +117,7 @@ lib/Model/OuterEnumInteger.php
 lib/Model/OuterEnumIntegerDefaultValue.php
 lib/Model/OuterObjectWithEnumProperty.php
 lib/Model/Pet.php
+lib/Model/PetWithFile.php
 lib/Model/PropertyNameMapping.php
 lib/Model/ReadOnlyFirst.php
 lib/Model/SingleRefType.php
@@ -124,3 +127,4 @@ lib/Model/TestInlineFreeformAdditionalPropertiesRequest.php
 lib/Model/User.php
 lib/ObjectSerializer.php
 phpunit.xml.dist
+test/Model/PetWithFileTest.php
diff --git a/samples/client/petstore/php/OpenAPIClient-php/README.md b/samples/client/petstore/php/OpenAPIClient-php/README.md
index d6f67751a753..116d18ca1bda 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/README.md
+++ b/samples/client/petstore/php/OpenAPIClient-php/README.md
@@ -108,6 +108,7 @@ Class | Method | HTTP request | Description
 *PetApi* | [**uploadFile**](docs/Api/PetApi.md#uploadfile) | **POST** /pet/{petId}/uploadImage | uploads an image
 *PetApi* | [**uploadFileWithRequiredFile**](docs/Api/PetApi.md#uploadfilewithrequiredfile) | **POST** /fake/{petId}/uploadImageWithRequiredFile | uploads an image (required)
 *PetApi* | [**uploadImageFullFormData**](docs/Api/PetApi.md#uploadimagefullformdata) | **POST** /pet/{petId}/uploadImageFullFormData | uploads an image attached to a Pet object as formdata
+*PetApi* | [**uploadImageFullFormDataNested**](docs/Api/PetApi.md#uploadimagefullformdatanested) | **POST** /pet/{petId}/uploadImageFullFormDataNested | uploads an image attached to a Pet object as formdata
 *StoreApi* | [**deleteOrder**](docs/Api/StoreApi.md#deleteorder) | **DELETE** /store/order/{order_id} | Delete purchase order by ID
 *StoreApi* | [**getInventory**](docs/Api/StoreApi.md#getinventory) | **GET** /store/inventory | Returns pet inventories by status
 *StoreApi* | [**getOrderById**](docs/Api/StoreApi.md#getorderbyid) | **GET** /store/order/{order_id} | Find purchase order by ID
@@ -166,6 +167,7 @@ Class | Method | HTTP request | Description
 - [OuterEnumIntegerDefaultValue](docs/Model/OuterEnumIntegerDefaultValue.md)
 - [OuterObjectWithEnumProperty](docs/Model/OuterObjectWithEnumProperty.md)
 - [Pet](docs/Model/Pet.md)
+- [PetWithFile](docs/Model/PetWithFile.md)
 - [PropertyNameMapping](docs/Model/PropertyNameMapping.md)
 - [ReadOnlyFirst](docs/Model/ReadOnlyFirst.md)
 - [SingleRefType](docs/Model/SingleRefType.md)
diff --git a/samples/client/petstore/php/OpenAPIClient-php/docs/Api/PetApi.md b/samples/client/petstore/php/OpenAPIClient-php/docs/Api/PetApi.md
index b81cba25df60..7e0512051f17 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/docs/Api/PetApi.md
+++ b/samples/client/petstore/php/OpenAPIClient-php/docs/Api/PetApi.md
@@ -14,6 +14,7 @@ All URIs are relative to http://petstore.swagger.io:80/v2, except if the operati
 | [**uploadFile()**](PetApi.md#uploadFile) | **POST** /pet/{petId}/uploadImage | uploads an image |
 | [**uploadFileWithRequiredFile()**](PetApi.md#uploadFileWithRequiredFile) | **POST** /fake/{petId}/uploadImageWithRequiredFile | uploads an image (required) |
 | [**uploadImageFullFormData()**](PetApi.md#uploadImageFullFormData) | **POST** /pet/{petId}/uploadImageFullFormData | uploads an image attached to a Pet object as formdata |
+| [**uploadImageFullFormDataNested()**](PetApi.md#uploadImageFullFormDataNested) | **POST** /pet/{petId}/uploadImageFullFormDataNested | uploads an image attached to a Pet object as formdata |
 
 
 ## `addPet()`
@@ -689,3 +690,65 @@ try {
 [[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
 [[Back to Model list]](../../README.md#models)
 [[Back to README]](../../README.md)
+
+## `uploadImageFullFormDataNested()`
+
+```php
+uploadImageFullFormDataNested($pet_id, $pet): \OpenAPI\Client\Model\ApiResponse
+```
+
+uploads an image attached to a Pet object as formdata
+
+
+
+### Example
+
+```php
+<?php
+require_once(__DIR__ . '/vendor/autoload.php');
+
+
+// Configure OAuth2 access token for authorization: petstore_auth
+$config = OpenAPI\Client\Configuration::getDefaultConfiguration()->setAccessToken('YOUR_ACCESS_TOKEN');
+
+
+$apiInstance = new OpenAPI\Client\Api\PetApi(
+    // If you want use custom http client, pass your client which implements `GuzzleHttp\ClientInterface`.
+    // This is optional, `GuzzleHttp\Client` will be used as default.
+    new GuzzleHttp\Client(),
+    $config
+);
+$pet_id = 56; // int | ID of pet to update
+$pet = new \OpenAPI\Client\Model\PetWithFile(); // \OpenAPI\Client\Model\PetWithFile
+
+try {
+    $result = $apiInstance->uploadImageFullFormDataNested($pet_id, $pet);
+    print_r($result);
+} catch (Exception $e) {
+    echo 'Exception when calling PetApi->uploadImageFullFormDataNested: ', $e->getMessage(), PHP_EOL;
+}
+```
+
+### Parameters
+
+| Name | Type | Description  | Notes |
+| ------------- | ------------- | ------------- | ------------- |
+| **pet_id** | **int**| ID of pet to update | |
+| **pet** | [**\OpenAPI\Client\Model\PetWithFile**](../Model/PetWithFile.md)|  | [optional] |
+
+### Return type
+
+[**\OpenAPI\Client\Model\ApiResponse**](../Model/ApiResponse.md)
+
+### Authorization
+
+[petstore_auth](../../README.md#petstore_auth)
+
+### HTTP request headers
+
+- **Content-Type**: `multipart/form-data`
+- **Accept**: `application/json`
+
+[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
+[[Back to Model list]](../../README.md#models)
+[[Back to README]](../../README.md)
diff --git a/samples/client/petstore/php/OpenAPIClient-php/docs/Model/PetWithFile.md b/samples/client/petstore/php/OpenAPIClient-php/docs/Model/PetWithFile.md
new file mode 100644
index 000000000000..520cd0b8725a
--- /dev/null
+++ b/samples/client/petstore/php/OpenAPIClient-php/docs/Model/PetWithFile.md
@@ -0,0 +1,16 @@
+# # PetWithFile
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**id** | **int** |  | [optional]
+**category** | [**\OpenAPI\Client\Model\Category**](Category.md) |  | [optional]
+**name** | **string** |  |
+**photo_urls** | **string[]** |  |
+**tags** | [**\OpenAPI\Client\Model\Tag[]**](Tag.md) |  | [optional]
+**status** | **string** | pet status in the store | [optional]
+**file** | **\SplFileObject** | file to upload | [optional]
+**multiple_files** | **\SplFileObject[]** |  | [optional]
+
+[[Back to Model list]](../../README.md#models) [[Back to API list]](../../README.md#endpoints) [[Back to README]](../../README.md)
diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php
index 7e12927282e4..96da50349ddd 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php
@@ -104,6 +104,9 @@ class PetApi
         'uploadImageFullFormData' => [
             'multipart/form-data',
         ],
+        'uploadImageFullFormDataNested' => [
+            'multipart/form-data',
+        ],
     ];
 
     /**
@@ -3359,6 +3362,334 @@ public function uploadImageFullFormDataRequest($pet_id, $name, $photo_urls, $id
         );
     }
 
+    /**
+     * Operation uploadImageFullFormDataNested
+     *
+     * uploads an image attached to a Pet object as formdata
+     *
+     * @param  int $pet_id ID of pet to update (required)
+     * @param  \OpenAPI\Client\Model\PetWithFile|null $pet pet (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['uploadImageFullFormDataNested'] to see the possible values for this operation
+     *
+     * @throws \OpenAPI\Client\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \OpenAPI\Client\Model\ApiResponse
+     */
+    public function uploadImageFullFormDataNested($pet_id, $pet = null, string $contentType = self::contentTypes['uploadImageFullFormDataNested'][0])
+    {
+        list($response) = $this->uploadImageFullFormDataNestedWithHttpInfo($pet_id, $pet, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation uploadImageFullFormDataNestedWithHttpInfo
+     *
+     * uploads an image attached to a Pet object as formdata
+     *
+     * @param  int $pet_id ID of pet to update (required)
+     * @param  \OpenAPI\Client\Model\PetWithFile|null $pet (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['uploadImageFullFormDataNested'] to see the possible values for this operation
+     *
+     * @throws \OpenAPI\Client\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \OpenAPI\Client\Model\ApiResponse, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function uploadImageFullFormDataNestedWithHttpInfo($pet_id, $pet = null, string $contentType = self::contentTypes['uploadImageFullFormDataNested'][0])
+    {
+        $request = $this->uploadImageFullFormDataNestedRequest($pet_id, $pet, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    if ('\OpenAPI\Client\Model\ApiResponse' === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ('\OpenAPI\Client\Model\ApiResponse' !== 'string') {
+                            try {
+                                $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR);
+                            } catch (\JsonException $exception) {
+                                throw new ApiException(
+                                    sprintf(
+                                        'Error JSON decoding server response (%s)',
+                                        $request->getUri()
+                                    ),
+                                    $statusCode,
+                                    $response->getHeaders(),
+                                    $content
+                                );
+                            }
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, '\OpenAPI\Client\Model\ApiResponse', []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+            }
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            $returnType = '\OpenAPI\Client\Model\ApiResponse';
+            if ($returnType === '\SplFileObject') {
+                $content = $response->getBody(); //stream goes to serializer
+            } else {
+                $content = (string) $response->getBody();
+                if ($returnType !== 'string') {
+                    try {
+                        $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR);
+                    } catch (\JsonException $exception) {
+                        throw new ApiException(
+                            sprintf(
+                                'Error JSON decoding server response (%s)',
+                                $request->getUri()
+                            ),
+                            $statusCode,
+                            $response->getHeaders(),
+                            $content
+                        );
+                    }
+                }
+            }
+
+            return [
+                ObjectSerializer::deserialize($content, $returnType, []),
+                $response->getStatusCode(),
+                $response->getHeaders()
+            ];
+
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\OpenAPI\Client\Model\ApiResponse',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    break;
+            }
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation uploadImageFullFormDataNestedAsync
+     *
+     * uploads an image attached to a Pet object as formdata
+     *
+     * @param  int $pet_id ID of pet to update (required)
+     * @param  \OpenAPI\Client\Model\PetWithFile|null $pet (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['uploadImageFullFormDataNested'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function uploadImageFullFormDataNestedAsync($pet_id, $pet = null, string $contentType = self::contentTypes['uploadImageFullFormDataNested'][0])
+    {
+        return $this->uploadImageFullFormDataNestedAsyncWithHttpInfo($pet_id, $pet, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation uploadImageFullFormDataNestedAsyncWithHttpInfo
+     *
+     * uploads an image attached to a Pet object as formdata
+     *
+     * @param  int $pet_id ID of pet to update (required)
+     * @param  \OpenAPI\Client\Model\PetWithFile|null $pet (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['uploadImageFullFormDataNested'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function uploadImageFullFormDataNestedAsyncWithHttpInfo($pet_id, $pet = null, string $contentType = self::contentTypes['uploadImageFullFormDataNested'][0])
+    {
+        $returnType = '\OpenAPI\Client\Model\ApiResponse';
+        $request = $this->uploadImageFullFormDataNestedRequest($pet_id, $pet, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'uploadImageFullFormDataNested'
+     *
+     * @param  int $pet_id ID of pet to update (required)
+     * @param  \OpenAPI\Client\Model\PetWithFile|null $pet (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['uploadImageFullFormDataNested'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function uploadImageFullFormDataNestedRequest($pet_id, $pet = null, string $contentType = self::contentTypes['uploadImageFullFormDataNested'][0])
+    {
+
+        // verify the required parameter 'pet_id' is set
+        if ($pet_id === null || (is_array($pet_id) && count($pet_id) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $pet_id when calling uploadImageFullFormDataNested'
+            );
+        }
+
+
+
+        $resourcePath = '/pet/{petId}/uploadImageFullFormDataNested';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+
+
+        // path params
+        if ($pet_id !== null) {
+            $resourcePath = str_replace(
+                '{' . 'petId' . '}',
+                ObjectSerializer::toPathValue($pet_id),
+                $resourcePath
+            );
+        }
+
+        // form params
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'pet' => $pet,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
+
+        $multipart = true;
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                $httpBody = \GuzzleHttp\Utils::jsonEncode($formParams);
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires OAuth (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'POST',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
     /**
      * Create http client option
      *
diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/FormDataProcessor.php b/samples/client/petstore/php/OpenAPIClient-php/lib/FormDataProcessor.php
new file mode 100644
index 000000000000..c416f584e6aa
--- /dev/null
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/FormDataProcessor.php
@@ -0,0 +1,242 @@
+<?php
+/**
+ * FormDataProcessor
+ * PHP version 7.4
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * OpenAPI Petstore
+ *
+ * This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.13.0-SNAPSHOT
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace OpenAPI\Client;
+
+use ArrayAccess;
+use DateTime;
+use GuzzleHttp\Psr7\Utils;
+use Psr\Http\Message\StreamInterface;
+use SplFileObject;
+use OpenAPI\Client\Model\ModelInterface;
+
+/**
+ * FormDataProcessor Class Doc Comment
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+class FormDataProcessor
+{
+    /**
+     * Tags whether payload passed to ::prepare() contains one or more
+     * SplFileObject or stream values.
+     */
+    public bool $has_file = false;
+
+    /**
+     * Take value and turn it into an array suitable for inclusion in
+     * the http body (form parameter). If it's a string, pass through unchanged
+     * If it's a datetime object, format it in ISO8601
+     *
+     * @param array<string|bool|array|DateTime|ArrayAccess|SplFileObject> $values the value of the form parameter
+     *
+     * @return array [key => value] of formdata
+     */
+    public function prepare(array $values): array
+    {
+        $this->has_file = false;
+        $result = [];
+
+        foreach ($values as $k => $v) {
+            if ($v === null) {
+                continue;
+            }
+
+            $result[$k] = $this->makeFormSafe($v);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Flattens a multi-level array of data and generates a single-level array
+     * compatible with formdata - a single-level array where the keys use bracket
+     * notation to signify nested data.
+     *
+     * credit: https://github.com/FranBar1966/FlatPHP
+     */
+    public static function flatten(array $source, string $start = ''): array
+    {
+        $opt = [
+            'prefix'          => '[',
+            'suffix'          => ']',
+            'suffix-end'      => true,
+            'prefix-list'     => '[',
+            'suffix-list'     => ']',
+            'suffix-list-end' => true,
+        ];
+
+        if ($start === '') {
+            $currentPrefix    = '';
+            $currentSuffix    = '';
+            $currentSuffixEnd = false;
+        } elseif (array_is_list($source)) {
+            $currentPrefix    = $opt['prefix-list'];
+            $currentSuffix    = $opt['suffix-list'];
+            $currentSuffixEnd = $opt['suffix-list-end'];
+        } else {
+            $currentPrefix    = $opt['prefix'];
+            $currentSuffix    = $opt['suffix'];
+            $currentSuffixEnd = $opt['suffix-end'];
+        }
+
+        $currentName = $start;
+        $result = [];
+
+        foreach ($source as $key => $val) {
+            $currentName .= $currentPrefix.$key;
+
+            if (is_array($val) && !empty($val)) {
+                $currentName .= $currentSuffix;
+                $result += self::flatten($val, $currentName);
+            } else {
+                if ($currentSuffixEnd) {
+                    $currentName .= $currentSuffix;
+                }
+
+                $result[$currentName] = ObjectSerializer::toString($val);
+            }
+
+            $currentName = $start;
+        }
+
+        return $result;
+    }
+
+    /**
+     * formdata must be limited to scalars or arrays of scalar values,
+     * or a resource for a file upload. Here we iterate through all available
+     * data and identify how to handle each scenario
+     */
+    protected function makeFormSafe($value)
+    {
+        if ($value instanceof SplFileObject) {
+            return $this->processFiles([$value])[0];
+        }
+
+        if (is_resource($value)) {
+            $this->has_file = true;
+
+            return $value;
+        }
+
+        if ($value instanceof ModelInterface) {
+            return $this->processModel($value);
+        }
+
+        if (is_array($value) || (is_object($value) && !$value instanceof \DateTimeInterface)) {
+            $data = [];
+
+            foreach ($value as $k => $v) {
+                $data[$k] = $this->makeFormSafe($v);
+            }
+
+            return $data;
+        }
+
+        return ObjectSerializer::toString($value);
+    }
+
+    /**
+     * We are able to handle nested ModelInterface. We do not simply call
+     * json_decode(json_encode()) because any given model may have binary data
+     * or other data that cannot be serialized to a JSON string
+     */
+    protected function processModel(ModelInterface $model): array
+    {
+        $result = [];
+
+        foreach ($model::openAPITypes() as $name => $type) {
+            $value = $model->offsetGet($name);
+
+            if ($value === null) {
+                continue;
+            }
+
+            if (strpos($type, '\SplFileObject') !== false) {
+                $file = is_array($value) ? $value : [$value];
+                $result[$name] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($value instanceof ModelInterface) {
+                $result[$name] = $this->processModel($value);
+
+                continue;
+            }
+
+            if (is_array($value) || is_object($value)) {
+                $result[$name] = $this->makeFormSafe($value);
+
+                continue;
+            }
+
+            $result[$name] = ObjectSerializer::toString($value);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Handle file data
+     */
+    protected function processFiles(array $files): array
+    {
+        $this->has_file = true;
+
+        $result = [];
+
+        foreach ($files as $i => $file) {
+            if (is_array($file)) {
+                $result[$i] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($file instanceof StreamInterface) {
+                $result[$i] = $file;
+
+                continue;
+            }
+
+            if ($file instanceof SplFileObject) {
+                $result[$i] = $this->tryFopen($file);
+            }
+        }
+
+        return $result;
+    }
+
+    private function tryFopen(SplFileObject $file)
+    {
+        return Utils::tryFopen($file->getRealPath(), 'rb');
+    }
+}
diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Model/PetWithFile.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Model/PetWithFile.php
new file mode 100644
index 000000000000..bc74a7add1e5
--- /dev/null
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Model/PetWithFile.php
@@ -0,0 +1,691 @@
+<?php
+/**
+ * PetWithFile
+ *
+ * PHP version 7.4
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * OpenAPI Petstore
+ *
+ * This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.13.0-SNAPSHOT
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace OpenAPI\Client\Model;
+
+use \ArrayAccess;
+use \OpenAPI\Client\ObjectSerializer;
+
+/**
+ * PetWithFile Class Doc Comment
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PetWithFile implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+      * The original name of the model.
+      *
+      * @var string
+      */
+    protected static $openAPIModelName = 'PetWithFile';
+
+    /**
+      * Array of property to type mappings. Used for (de)serialization
+      *
+      * @var string[]
+      */
+    protected static $openAPITypes = [
+        'id' => 'int',
+        'category' => '\OpenAPI\Client\Model\Category',
+        'name' => 'string',
+        'photo_urls' => 'string[]',
+        'tags' => '\OpenAPI\Client\Model\Tag[]',
+        'status' => 'string',
+        'file' => '\SplFileObject',
+        'multiple_files' => '\SplFileObject[]'
+    ];
+
+    /**
+      * Array of property to format mappings. Used for (de)serialization
+      *
+      * @var string[]
+      * @phpstan-var array<string, string|null>
+      * @psalm-var array<string, string|null>
+      */
+    protected static $openAPIFormats = [
+        'id' => 'int64',
+        'category' => null,
+        'name' => null,
+        'photo_urls' => null,
+        'tags' => null,
+        'status' => null,
+        'file' => 'binary',
+        'multiple_files' => 'binary'
+    ];
+
+    /**
+      * Array of nullable properties. Used for (de)serialization
+      *
+      * @var boolean[]
+      */
+    protected static array $openAPINullables = [
+        'id' => false,
+        'category' => false,
+        'name' => false,
+        'photo_urls' => false,
+        'tags' => false,
+        'status' => false,
+        'file' => false,
+        'multiple_files' => false
+    ];
+
+    /**
+      * If a nullable field gets set to null, insert it here
+      *
+      * @var boolean[]
+      */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'id' => 'id',
+        'category' => 'category',
+        'name' => 'name',
+        'photo_urls' => 'photoUrls',
+        'tags' => 'tags',
+        'status' => 'status',
+        'file' => 'file',
+        'multiple_files' => 'multiple_files'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'id' => 'setId',
+        'category' => 'setCategory',
+        'name' => 'setName',
+        'photo_urls' => 'setPhotoUrls',
+        'tags' => 'setTags',
+        'status' => 'setStatus',
+        'file' => 'setFile',
+        'multiple_files' => 'setMultipleFiles'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'id' => 'getId',
+        'category' => 'getCategory',
+        'name' => 'getName',
+        'photo_urls' => 'getPhotoUrls',
+        'tags' => 'getTags',
+        'status' => 'getStatus',
+        'file' => 'getFile',
+        'multiple_files' => 'getMultipleFiles'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+    public const STATUS_AVAILABLE = 'available';
+    public const STATUS_PENDING = 'pending';
+    public const STATUS_SOLD = 'sold';
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getStatusAllowableValues()
+    {
+        return [
+            self::STATUS_AVAILABLE,
+            self::STATUS_PENDING,
+            self::STATUS_SOLD,
+        ];
+    }
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('id', $data ?? [], null);
+        $this->setIfExists('category', $data ?? [], null);
+        $this->setIfExists('name', $data ?? [], null);
+        $this->setIfExists('photo_urls', $data ?? [], null);
+        $this->setIfExists('tags', $data ?? [], null);
+        $this->setIfExists('status', $data ?? [], null);
+        $this->setIfExists('file', $data ?? [], null);
+        $this->setIfExists('multiple_files', $data ?? [], null);
+    }
+
+    /**
+    * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+    * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+    * $this->openAPINullablesSetToNull array
+    *
+    * @param string $variableName
+    * @param array  $fields
+    * @param mixed  $defaultValue
+    */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        if ($this->container['name'] === null) {
+            $invalidProperties[] = "'name' can't be null";
+        }
+        if ($this->container['photo_urls'] === null) {
+            $invalidProperties[] = "'photo_urls' can't be null";
+        }
+        $allowedValues = $this->getStatusAllowableValues();
+        if (!is_null($this->container['status']) && !in_array($this->container['status'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'status', must be one of '%s'",
+                $this->container['status'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets id
+     *
+     * @return int|null
+     */
+    public function getId()
+    {
+        return $this->container['id'];
+    }
+
+    /**
+     * Sets id
+     *
+     * @param int|null $id id
+     *
+     * @return self
+     */
+    public function setId($id)
+    {
+        if (is_null($id)) {
+            throw new \InvalidArgumentException('non-nullable id cannot be null');
+        }
+        $this->container['id'] = $id;
+
+        return $this;
+    }
+
+    /**
+     * Gets category
+     *
+     * @return \OpenAPI\Client\Model\Category|null
+     */
+    public function getCategory()
+    {
+        return $this->container['category'];
+    }
+
+    /**
+     * Sets category
+     *
+     * @param \OpenAPI\Client\Model\Category|null $category category
+     *
+     * @return self
+     */
+    public function setCategory($category)
+    {
+        if (is_null($category)) {
+            throw new \InvalidArgumentException('non-nullable category cannot be null');
+        }
+        $this->container['category'] = $category;
+
+        return $this;
+    }
+
+    /**
+     * Gets name
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->container['name'];
+    }
+
+    /**
+     * Sets name
+     *
+     * @param string $name name
+     *
+     * @return self
+     */
+    public function setName($name)
+    {
+        if (is_null($name)) {
+            throw new \InvalidArgumentException('non-nullable name cannot be null');
+        }
+        $this->container['name'] = $name;
+
+        return $this;
+    }
+
+    /**
+     * Gets photo_urls
+     *
+     * @return string[]
+     */
+    public function getPhotoUrls()
+    {
+        return $this->container['photo_urls'];
+    }
+
+    /**
+     * Sets photo_urls
+     *
+     * @param string[] $photo_urls photo_urls
+     *
+     * @return self
+     */
+    public function setPhotoUrls($photo_urls)
+    {
+        if (is_null($photo_urls)) {
+            throw new \InvalidArgumentException('non-nullable photo_urls cannot be null');
+        }
+
+
+        $this->container['photo_urls'] = $photo_urls;
+
+        return $this;
+    }
+
+    /**
+     * Gets tags
+     *
+     * @return \OpenAPI\Client\Model\Tag[]|null
+     */
+    public function getTags()
+    {
+        return $this->container['tags'];
+    }
+
+    /**
+     * Sets tags
+     *
+     * @param \OpenAPI\Client\Model\Tag[]|null $tags tags
+     *
+     * @return self
+     */
+    public function setTags($tags)
+    {
+        if (is_null($tags)) {
+            throw new \InvalidArgumentException('non-nullable tags cannot be null');
+        }
+        $this->container['tags'] = $tags;
+
+        return $this;
+    }
+
+    /**
+     * Gets status
+     *
+     * @return string|null
+     */
+    public function getStatus()
+    {
+        return $this->container['status'];
+    }
+
+    /**
+     * Sets status
+     *
+     * @param string|null $status pet status in the store
+     *
+     * @return self
+     */
+    public function setStatus($status)
+    {
+        if (is_null($status)) {
+            throw new \InvalidArgumentException('non-nullable status cannot be null');
+        }
+        $allowedValues = $this->getStatusAllowableValues();
+        if (!in_array($status, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'status', must be one of '%s'",
+                    $status,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['status'] = $status;
+
+        return $this;
+    }
+
+    /**
+     * Gets file
+     *
+     * @return \SplFileObject|null
+     */
+    public function getFile()
+    {
+        return $this->container['file'];
+    }
+
+    /**
+     * Sets file
+     *
+     * @param \SplFileObject|null $file file to upload
+     *
+     * @return self
+     */
+    public function setFile($file)
+    {
+        if (is_null($file)) {
+            throw new \InvalidArgumentException('non-nullable file cannot be null');
+        }
+        $this->container['file'] = $file;
+
+        return $this;
+    }
+
+    /**
+     * Gets multiple_files
+     *
+     * @return \SplFileObject[]|null
+     */
+    public function getMultipleFiles()
+    {
+        return $this->container['multiple_files'];
+    }
+
+    /**
+     * Sets multiple_files
+     *
+     * @param \SplFileObject[]|null $multiple_files multiple_files
+     *
+     * @return self
+     */
+    public function setMultipleFiles($multiple_files)
+    {
+        if (is_null($multiple_files)) {
+            throw new \InvalidArgumentException('non-nullable multiple_files cannot be null');
+        }
+        $this->container['multiple_files'] = $multiple_files;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists($offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet($offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset($offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+
diff --git a/samples/client/petstore/php/OpenAPIClient-php/test/Model/PetWithFileTest.php b/samples/client/petstore/php/OpenAPIClient-php/test/Model/PetWithFileTest.php
new file mode 100644
index 000000000000..ce0a73f294d0
--- /dev/null
+++ b/samples/client/petstore/php/OpenAPIClient-php/test/Model/PetWithFileTest.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * PetWithFileTest
+ *
+ * PHP version 7.4
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * OpenAPI Petstore
+ *
+ * This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.13.0-SNAPSHOT
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Please update the test case below to test the model.
+ */
+
+namespace OpenAPI\Client\Test\Model;
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * PetWithFileTest Class Doc Comment
+ *
+ * @category    Class
+ * @description PetWithFile
+ * @package     OpenAPI\Client
+ * @author      OpenAPI Generator team
+ * @link        https://openapi-generator.tech
+ */
+class PetWithFileTest extends TestCase
+{
+
+    /**
+     * Setup before running any test case
+     */
+    public static function setUpBeforeClass(): void
+    {
+    }
+
+    /**
+     * Setup before running each test case
+     */
+    public function setUp(): void
+    {
+    }
+
+    /**
+     * Clean up after running each test case
+     */
+    public function tearDown(): void
+    {
+    }
+
+    /**
+     * Clean up after running all test cases
+     */
+    public static function tearDownAfterClass(): void
+    {
+    }
+
+    /**
+     * Test "PetWithFile"
+     */
+    public function testPetWithFile()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "id"
+     */
+    public function testPropertyId()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "category"
+     */
+    public function testPropertyCategory()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "name"
+     */
+    public function testPropertyName()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "photo_urls"
+     */
+    public function testPropertyPhotoUrls()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "tags"
+     */
+    public function testPropertyTags()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "status"
+     */
+    public function testPropertyStatus()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "file"
+     */
+    public function testPropertyFile()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "multiple_files"
+     */
+    public function testPropertyMultipleFiles()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+}
diff --git a/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php b/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php
new file mode 100644
index 000000000000..501b61101254
--- /dev/null
+++ b/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace OpenAPI\Client;
+
+use DateTime;
+use PHPUnit\Framework\TestCase;
+use SplFileObject;
+
+/**
+ * class FormDataProcessorTest
+ *
+ * @package OpenAPI\Client
+ */
+class FormDataProcessorTest extends TestCase
+{
+    /**
+     * @dataProvider providerFlatten
+     */
+    public function testFlatten($data, array $expected): void
+    {
+        $formDataProcessor = new FormDataProcessor();
+        $formData = $formDataProcessor->prepare($data);
+
+        $result = $formDataProcessor::flatten($formData);
+
+        $this->assertEquals($expected, $result);
+    }
+
+    public function providerFlatten(): iterable
+    {
+        $data = [
+            'id'         => '1234',
+            'name'       => 'Spike',
+            'photo_urls' => [
+                'https://example.com/picture_1.jpg',
+                'https://example.com/picture_2.jpg',
+            ],
+            'status'     => Model\Pet::STATUS_AVAILABLE,
+            'category'   => [
+                'id'   => '12345',
+                'name' => 'Category_Name',
+            ],
+            'tags'       => [
+                [
+                    'id'   => '12345',
+                    'name' => 'tag_1',
+                ],
+                [
+                    'id'   => '98765',
+                    'name' => 'tag_2',
+                ],
+            ],
+        ];
+
+        yield [
+            'data'     => $data,
+            'expected' => [
+                'id'             => $data['id'],
+                'name'           => $data['name'],
+                'photo_urls[0]'  => $data['photo_urls'][0],
+                'photo_urls[1]'  => $data['photo_urls'][1],
+                'status'         => $data['status'],
+                'category[id]'   => (string) $data['category']['id'],
+                'category[name]' => $data['category']['name'],
+                'tags[0][id]'    => (string) $data['tags'][0]['id'],
+                'tags[0][name]'  => $data['tags'][0]['name'],
+                'tags[1][id]'    => (string) $data['tags'][1]['id'],
+                'tags[1][name]'  => $data['tags'][1]['name'],
+            ],
+        ];
+
+        $category = (new Model\Category())
+            ->setId($data['category']['id'])
+            ->setName($data['category']['name']);
+
+        $tags_1 = (new Model\Tag())
+            ->setId($data['tags'][0]['id'])
+            ->setName($data['tags'][0]['name']);
+
+        $tags_2 = (new Model\Tag())
+            ->setId($data['tags'][1]['id'])
+            ->setName($data['tags'][1]['name']);
+
+        $tags = [
+            $tags_1,
+            $tags_2,
+        ];
+
+        $pet = new Model\Pet([]);
+        $pet->setId($data['id'])
+            ->setName($data['name'])
+            ->setPhotoUrls($data['photo_urls'])
+            ->setStatus($data['status'])
+            ->setCategory($category)
+            ->setTags($tags);
+
+        yield [
+            'data'     => ['pet' => $pet],
+            'expected' => [
+                'pet[id]'             => $data['id'],
+                'pet[name]'           => $data['name'],
+                'pet[photo_urls][0]'  => $data['photo_urls'][0],
+                'pet[photo_urls][1]'  => $data['photo_urls'][1],
+                'pet[status]'         => $data['status'],
+                'pet[category][id]'   => (string) $data['category']['id'],
+                'pet[category][name]' => $data['category']['name'],
+                'pet[tags][0][id]'    => (string) $data['tags'][0]['id'],
+                'pet[tags][0][name]'  => $data['tags'][0]['name'],
+                'pet[tags][1][id]'    => (string) $data['tags'][1]['id'],
+                'pet[tags][1][name]'  => $data['tags'][1]['name'],
+            ],
+        ];
+
+        yield [
+            'data'     => ['key' => new DateTime('2021-10-06T20:17:16')],
+            'expected' => ['key' => '2021-10-06T20:17:16+00:00'],
+        ];
+
+        yield [
+            'data'     => ['key' => true],
+            'expected' => ['key' => 'true'],
+        ];
+
+        yield [
+            'data'     => ['key' => false],
+            'expected' => ['key' => 'false'],
+        ];
+
+        yield [
+            'data'     => ['key' => 'some value'],
+            'expected' => ['key' => 'some value'],
+        ];
+    }
+
+    public function testHasFile(): void
+    {
+        $filepath = realpath(__DIR__ . '/../.openapi-generator/VERSION');
+        $file = new SplFileObject($filepath);
+
+        $pet = new Model\PetWithFile([]);
+        $pet->setId(123)
+            ->setName('Spike')
+            ->setFile($file);
+
+        $formDataProcessor = new FormDataProcessor();
+
+        $this->assertFalse($formDataProcessor->has_file);
+        $formData = $formDataProcessor->prepare(['pet' => $pet]);
+
+        $this->assertIsResource($formData['pet']['file'][0]);
+        $this->assertTrue($formDataProcessor->has_file);
+    }
+
+    public function testHasFileMultiple(): void
+    {
+        $filepath = realpath(__DIR__ . '/../.openapi-generator/VERSION');
+        $file = new SplFileObject($filepath);
+
+        $pet = new Model\PetWithFile([]);
+        $pet->setId(123)
+            ->setName('Spike')
+            ->setMultipleFiles([$file]);
+
+        $formDataProcessor = new FormDataProcessor();
+
+        $this->assertFalse($formDataProcessor->has_file);
+        $formData = $formDataProcessor->prepare(['pet' => $pet]);
+
+        $this->assertIsResource($formData['pet']['multiple_files'][0]);
+        $this->assertTrue($formDataProcessor->has_file);
+    }
+}
diff --git a/samples/client/petstore/php/OpenAPIClient-php/tests/ObjectSerializerTest.php b/samples/client/petstore/php/OpenAPIClient-php/tests/ObjectSerializerTest.php
index 47b94f12f4fa..dc71cd108b85 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/tests/ObjectSerializerTest.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/tests/ObjectSerializerTest.php
@@ -636,98 +636,4 @@ public function testArrayGivenAsObjectForDeserialize(): void
         $tag = $tags[0];
         $this->assertInstanceOf(Tag::class, $tag);
     }
-
-    /**
-     * @dataProvider providerToFormValue
-     */
-    public function testToFormValue(
-        mixed $data,
-        mixed $expected,
-    ): void {
-        $result = ObjectSerializer::toFormValue('key', $data);
-
-        $this->assertEquals($expected, $result);
-    }
-
-    public function providerToFormValue(): iterable
-    {
-        yield [
-            'data'     => new DateTime('2021-10-06T20:17:16'),
-            'expected' => ['key' => '2021-10-06T20:17:16+00:00'],
-        ];
-
-        yield [
-            'data'     => true,
-            'expected' => ['key' => 'true'],
-        ];
-
-        yield [
-            'data'     => false,
-            'expected' => ['key' => 'false'],
-        ];
-
-        yield [
-            'data'     => 'some value',
-            'expected' => ['key' => 'some value'],
-        ];
-
-        $filepath = realpath(__DIR__ . '/../.openapi-generator/VERSION');
-        $file = new \SplFileObject($filepath);
-
-        yield [
-            'data'     => $file,
-            'expected' => ['key' => $filepath],
-        ];
-
-        $id = 1234;
-        $name = 'Spike';
-
-        $category = (new Model\Category())
-            ->setId(12345)
-            ->setName("Category_Name");
-
-        $tags_1 = (new Model\Tag())
-            ->setId(12345)
-            ->setName("tag_1");
-
-        $tags_2 = (new Model\Tag())
-            ->setId(98765)
-            ->setName("tag_2");
-
-        $tags = [
-            $tags_1,
-            $tags_2,
-        ];
-
-        $photo_urls = [
-            "https://example.com/picture_1.jpg",
-            "https://example.com/picture_2.jpg",
-        ];
-        $status = Model\Pet::STATUS_AVAILABLE;
-
-        $pet = new Model\Pet([]);
-        $pet->setId($id)
-            ->setName($name)
-            ->setPhotoUrls($photo_urls)
-            ->setStatus($status)
-            ->setCategory($category)
-            ->setTags($tags);
-
-        yield [
-            'data'     => $pet,
-            'expected' => [
-                'key[id]'             => "{$id}",
-                'key[name]'           => $name,
-                'key[photoUrls][0]'   => $photo_urls[0],
-                'key[photoUrls][1]'   => $photo_urls[1],
-                'key[status]'         => $status,
-                'key[category][id]'   => "{$category->getId()}",
-                'key[category][name]' => $category->getName(),
-                'key[tags][0][id]'    => "{$tags_1->getId()}",
-                'key[tags][0][name]'  => $tags_1->getName(),
-                'key[tags][1][id]'    => "{$tags_2->getId()}",
-                'key[tags][1][name]'  => $tags_2->getName(),
-            ],
-        ];
-    }
 }
diff --git a/samples/client/petstore/php/psr-18/.openapi-generator/FILES b/samples/client/petstore/php/psr-18/.openapi-generator/FILES
index 26dfe36c4c49..404c4d9c109c 100644
--- a/samples/client/petstore/php/psr-18/.openapi-generator/FILES
+++ b/samples/client/petstore/php/psr-18/.openapi-generator/FILES
@@ -53,6 +53,7 @@ docs/Model/OuterEnumInteger.md
 docs/Model/OuterEnumIntegerDefaultValue.md
 docs/Model/OuterObjectWithEnumProperty.md
 docs/Model/Pet.md
+docs/Model/PetWithFile.md
 docs/Model/PropertyNameMapping.md
 docs/Model/ReadOnlyFirst.md
 docs/Model/SingleRefType.md
@@ -71,6 +72,7 @@ lib/Api/UserApi.php
 lib/ApiException.php
 lib/Configuration.php
 lib/DebugPlugin.php
+lib/FormDataProcessor.php
 lib/HeaderSelector.php
 lib/Model/AdditionalPropertiesClass.php
 lib/Model/AllOfWithSingleRef.php
@@ -116,6 +118,7 @@ lib/Model/OuterEnumInteger.php
 lib/Model/OuterEnumIntegerDefaultValue.php
 lib/Model/OuterObjectWithEnumProperty.php
 lib/Model/Pet.php
+lib/Model/PetWithFile.php
 lib/Model/PropertyNameMapping.php
 lib/Model/ReadOnlyFirst.php
 lib/Model/SingleRefType.php
@@ -125,3 +128,4 @@ lib/Model/TestInlineFreeformAdditionalPropertiesRequest.php
 lib/Model/User.php
 lib/ObjectSerializer.php
 phpunit.xml.dist
+test/Model/PetWithFileTest.php
diff --git a/samples/client/petstore/php/psr-18/README.md b/samples/client/petstore/php/psr-18/README.md
index 1c8d4f2541ce..c29839b3ba01 100644
--- a/samples/client/petstore/php/psr-18/README.md
+++ b/samples/client/petstore/php/psr-18/README.md
@@ -119,6 +119,7 @@ Class | Method | HTTP request | Description
 *PetApi* | [**uploadFile**](docs/Api/PetApi.md#uploadfile) | **POST** /pet/{petId}/uploadImage | uploads an image
 *PetApi* | [**uploadFileWithRequiredFile**](docs/Api/PetApi.md#uploadfilewithrequiredfile) | **POST** /fake/{petId}/uploadImageWithRequiredFile | uploads an image (required)
 *PetApi* | [**uploadImageFullFormData**](docs/Api/PetApi.md#uploadimagefullformdata) | **POST** /pet/{petId}/uploadImageFullFormData | uploads an image attached to a Pet object as formdata
+*PetApi* | [**uploadImageFullFormDataNested**](docs/Api/PetApi.md#uploadimagefullformdatanested) | **POST** /pet/{petId}/uploadImageFullFormDataNested | uploads an image attached to a Pet object as formdata
 *StoreApi* | [**deleteOrder**](docs/Api/StoreApi.md#deleteorder) | **DELETE** /store/order/{order_id} | Delete purchase order by ID
 *StoreApi* | [**getInventory**](docs/Api/StoreApi.md#getinventory) | **GET** /store/inventory | Returns pet inventories by status
 *StoreApi* | [**getOrderById**](docs/Api/StoreApi.md#getorderbyid) | **GET** /store/order/{order_id} | Find purchase order by ID
@@ -177,6 +178,7 @@ Class | Method | HTTP request | Description
 - [OuterEnumIntegerDefaultValue](docs/Model/OuterEnumIntegerDefaultValue.md)
 - [OuterObjectWithEnumProperty](docs/Model/OuterObjectWithEnumProperty.md)
 - [Pet](docs/Model/Pet.md)
+- [PetWithFile](docs/Model/PetWithFile.md)
 - [PropertyNameMapping](docs/Model/PropertyNameMapping.md)
 - [ReadOnlyFirst](docs/Model/ReadOnlyFirst.md)
 - [SingleRefType](docs/Model/SingleRefType.md)
diff --git a/samples/client/petstore/php/psr-18/docs/Api/PetApi.md b/samples/client/petstore/php/psr-18/docs/Api/PetApi.md
index 1b5a4c942fd5..c893be3b0e0b 100644
--- a/samples/client/petstore/php/psr-18/docs/Api/PetApi.md
+++ b/samples/client/petstore/php/psr-18/docs/Api/PetApi.md
@@ -14,6 +14,7 @@ Method | HTTP request | Description
 [**uploadFile()**](PetApi.md#uploadFile) | **POST** /pet/{petId}/uploadImage | uploads an image
 [**uploadFileWithRequiredFile()**](PetApi.md#uploadFileWithRequiredFile) | **POST** /fake/{petId}/uploadImageWithRequiredFile | uploads an image (required)
 [**uploadImageFullFormData()**](PetApi.md#uploadImageFullFormData) | **POST** /pet/{petId}/uploadImageFullFormData | uploads an image attached to a Pet object as formdata
+[**uploadImageFullFormDataNested()**](PetApi.md#uploadImageFullFormDataNested) | **POST** /pet/{petId}/uploadImageFullFormDataNested | uploads an image attached to a Pet object as formdata
 
 
 ## `addPet()`
@@ -643,3 +644,65 @@ Name | Type | Description  | Notes
 [[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
 [[Back to Model list]](../../README.md#models)
 [[Back to README]](../../README.md)
+
+## `uploadImageFullFormDataNested()`
+
+```php
+uploadImageFullFormDataNested($pet_id, $pet): \OpenAPI\Client\Model\ApiResponse
+```
+
+uploads an image attached to a Pet object as formdata
+
+
+
+### Example
+
+```php
+<?php
+require_once(__DIR__ . '/vendor/autoload.php');
+
+
+// Configure OAuth2 access token for authorization: petstore_auth
+$config = OpenAPI\Client\Configuration::getDefaultConfiguration()->setAccessToken('YOUR_ACCESS_TOKEN');
+
+
+$apiInstance = new OpenAPI\Client\Api\PetApi(
+    // If you want use custom http client, pass your client which implements `Psr\Http\Client\ClientInterface`.
+    // This is optional, `Psr18ClientDiscovery` will be used to find http client. For instance `GuzzleHttp\Client` implements that interface
+    new GuzzleHttp\Client(),
+    $config
+);
+$pet_id = 56; // int | ID of pet to update
+$pet = new \OpenAPI\Client\Model\PetWithFile(); // \OpenAPI\Client\Model\PetWithFile
+
+try {
+    $result = $apiInstance->uploadImageFullFormDataNested($pet_id, $pet);
+    print_r($result);
+} catch (Exception $e) {
+    echo 'Exception when calling PetApi->uploadImageFullFormDataNested: ', $e->getMessage(), PHP_EOL;
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **pet_id** | **int**| ID of pet to update |
+ **pet** | [**\OpenAPI\Client\Model\PetWithFile**](../Model/PetWithFile.md)|  | [optional]
+
+### Return type
+
+[**\OpenAPI\Client\Model\ApiResponse**](../Model/ApiResponse.md)
+
+### Authorization
+
+[petstore_auth](../../README.md#petstore_auth)
+
+### HTTP request headers
+
+- **Content-Type**: `multipart/form-data`
+- **Accept**: `application/json`
+
+[[Back to top]](#) [[Back to API list]](../../README.md#endpoints)
+[[Back to Model list]](../../README.md#models)
+[[Back to README]](../../README.md)
diff --git a/samples/client/petstore/php/psr-18/docs/Model/PetWithFile.md b/samples/client/petstore/php/psr-18/docs/Model/PetWithFile.md
new file mode 100644
index 000000000000..520cd0b8725a
--- /dev/null
+++ b/samples/client/petstore/php/psr-18/docs/Model/PetWithFile.md
@@ -0,0 +1,16 @@
+# # PetWithFile
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**id** | **int** |  | [optional]
+**category** | [**\OpenAPI\Client\Model\Category**](Category.md) |  | [optional]
+**name** | **string** |  |
+**photo_urls** | **string[]** |  |
+**tags** | [**\OpenAPI\Client\Model\Tag[]**](Tag.md) |  | [optional]
+**status** | **string** | pet status in the store | [optional]
+**file** | **\SplFileObject** | file to upload | [optional]
+**multiple_files** | **\SplFileObject[]** |  | [optional]
+
+[[Back to Model list]](../../README.md#models) [[Back to API list]](../../README.md#endpoints) [[Back to README]](../../README.md)
diff --git a/samples/client/petstore/php/psr-18/lib/Api/PetApi.php b/samples/client/petstore/php/psr-18/lib/Api/PetApi.php
index 68514f74315e..84dc80a9011c 100644
--- a/samples/client/petstore/php/psr-18/lib/Api/PetApi.php
+++ b/samples/client/petstore/php/psr-18/lib/Api/PetApi.php
@@ -2771,6 +2771,278 @@ public function uploadImageFullFormDataRequest($pet_id, $name, $photo_urls, $id
         return $this->createRequest('POST', $uri, $headers, $httpBody);
     }
 
+    /**
+     * Operation uploadImageFullFormDataNested
+     *
+     * uploads an image attached to a Pet object as formdata
+     *
+     * @param  int $pet_id ID of pet to update (required)
+     * @param  \OpenAPI\Client\Model\PetWithFile $pet pet (optional)
+     *
+     * @throws \OpenAPI\Client\ApiException on non-2xx response
+     * @throws \InvalidArgumentException
+     * @return \OpenAPI\Client\Model\ApiResponse
+     */
+    public function uploadImageFullFormDataNested($pet_id, $pet = null)
+    {
+        list($response) = $this->uploadImageFullFormDataNestedWithHttpInfo($pet_id, $pet);
+        return $response;
+    }
+
+    /**
+     * Operation uploadImageFullFormDataNestedWithHttpInfo
+     *
+     * uploads an image attached to a Pet object as formdata
+     *
+     * @param  int $pet_id ID of pet to update (required)
+     * @param  \OpenAPI\Client\Model\PetWithFile $pet (optional)
+     *
+     * @throws \OpenAPI\Client\ApiException on non-2xx response
+     * @throws \InvalidArgumentException
+     * @return array of \OpenAPI\Client\Model\ApiResponse, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function uploadImageFullFormDataNestedWithHttpInfo($pet_id, $pet = null)
+    {
+        $request = $this->uploadImageFullFormDataNestedRequest($pet_id, $pet);
+
+        try {
+            try {
+                $response = $this->httpClient->sendRequest($request);
+            } catch (HttpException $e) {
+                $response = $e->getResponse();
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $response->getStatusCode(),
+                        (string) $request->getUri()
+                    ),
+                    $request,
+                    $response,
+                    $e
+                );
+            } catch (ClientExceptionInterface $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    $request,
+                    null,
+                    $e
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+            switch($statusCode) {
+                case 200:
+                    if ('\OpenAPI\Client\Model\ApiResponse' === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, '\OpenAPI\Client\Model\ApiResponse', []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+            }
+
+            $returnType = '\OpenAPI\Client\Model\ApiResponse';
+            if ($returnType === '\SplFileObject') {
+                $content = $response->getBody(); //stream goes to serializer
+            } else {
+                $content = (string) $response->getBody();
+            }
+
+            return [
+                ObjectSerializer::deserialize($content, $returnType, []),
+                $response->getStatusCode(),
+                $response->getHeaders()
+            ];
+
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\OpenAPI\Client\Model\ApiResponse',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    break;
+            }
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation uploadImageFullFormDataNestedAsync
+     *
+     * uploads an image attached to a Pet object as formdata
+     *
+     * @param  int $pet_id ID of pet to update (required)
+     * @param  \OpenAPI\Client\Model\PetWithFile $pet (optional)
+     *
+     * @throws \InvalidArgumentException
+     * @return Promise
+     */
+    public function uploadImageFullFormDataNestedAsync($pet_id, $pet = null)
+    {
+        return $this->uploadImageFullFormDataNestedAsyncWithHttpInfo($pet_id, $pet)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation uploadImageFullFormDataNestedAsyncWithHttpInfo
+     *
+     * uploads an image attached to a Pet object as formdata
+     *
+     * @param  int $pet_id ID of pet to update (required)
+     * @param  \OpenAPI\Client\Model\PetWithFile $pet (optional)
+     *
+     * @throws \InvalidArgumentException
+     * @return Promise
+     */
+    public function uploadImageFullFormDataNestedAsyncWithHttpInfo($pet_id, $pet = null)
+    {
+        $returnType = '\OpenAPI\Client\Model\ApiResponse';
+        $request = $this->uploadImageFullFormDataNestedRequest($pet_id, $pet);
+
+        return $this->httpAsyncClient->sendAsyncRequest($request)
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function (HttpException $exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $exception->getRequest(),
+                        $exception->getResponse(),
+                        $exception
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'uploadImageFullFormDataNested'
+     *
+     * @param  int $pet_id ID of pet to update (required)
+     * @param  \OpenAPI\Client\Model\PetWithFile $pet (optional)
+     *
+     * @throws \InvalidArgumentException
+     * @return RequestInterface
+     */
+    public function uploadImageFullFormDataNestedRequest($pet_id, $pet = null)
+    {
+        // verify the required parameter 'pet_id' is set
+        if ($pet_id === null || (is_array($pet_id) && count($pet_id) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $pet_id when calling uploadImageFullFormDataNested'
+            );
+        }
+
+        $resourcePath = '/pet/{petId}/uploadImageFullFormDataNested';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = null;
+        $multipart = false;
+
+
+
+        // path params
+        if ($pet_id !== null) {
+            $resourcePath = str_replace(
+                '{' . 'petId' . '}',
+                ObjectSerializer::toPathValue($pet_id),
+                $resourcePath
+            );
+        }
+
+        // form params
+        $formDataProcessor = new FormDataProcessor();
+
+        $formData = $formDataProcessor->prepare([
+            'pet' => $pet,
+        ]);
+
+        $formParams = $formDataProcessor->flatten($formData);
+        $multipart = $formDataProcessor->has_file;
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json'],
+            'multipart/form-data',
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif ($this->headerSelector->isJsonMime($headers['Content-Type'])) {
+                $httpBody = json_encode($formParams);
+
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires OAuth (access token)
+        if ($this->config->getAccessToken() !== null) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+
+        $uri = $this->createUri($operationHost, $resourcePath, $queryParams);
+
+        return $this->createRequest('POST', $uri, $headers, $httpBody);
+    }
+
 
     /**
      * @param string $method
diff --git a/samples/client/petstore/php/psr-18/lib/FormDataProcessor.php b/samples/client/petstore/php/psr-18/lib/FormDataProcessor.php
new file mode 100644
index 000000000000..34a446123bcf
--- /dev/null
+++ b/samples/client/petstore/php/psr-18/lib/FormDataProcessor.php
@@ -0,0 +1,242 @@
+<?php
+/**
+ * FormDataProcessor
+ * PHP version 7.4
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * OpenAPI Petstore
+ *
+ * This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.13.0-SNAPSHOT
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace OpenAPI\Client;
+
+use ArrayAccess;
+use DateTime;
+use GuzzleHttp\Psr7\Utils;
+use Psr\Http\Message\StreamInterface;
+use SplFileObject;
+use OpenAPI\Client\Model\ModelInterface;
+
+/**
+ * FormDataProcessor Class Doc Comment
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+class FormDataProcessor
+{
+    /**
+     * Tags whether payload passed to ::prepare() contains one or more
+     * SplFileObject or stream values.
+     */
+    public bool $has_file = false;
+
+    /**
+     * Take value and turn it into an array suitable for inclusion in
+     * the http body (form parameter). If it's a string, pass through unchanged
+     * If it's a datetime object, format it in ISO8601
+     *
+     * @param array<string|bool|array|DateTime|ArrayAccess|SplFileObject> $values the value of the form parameter
+     *
+     * @return array [key => value] of formdata
+     */
+    public function prepare(array $values): array
+    {
+        $this->has_file = false;
+        $result = [];
+
+        foreach ($values as $k => $v) {
+            if ($v === null) {
+                continue;
+            }
+
+            $result[$k] = $this->makeFormSafe($v);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Flattens a multi-level array of data and generates a single-level array
+     * compatible with formdata - a single-level array where the keys use bracket
+     * notation to signify nested data.
+     *
+     * credit: https://github.com/FranBar1966/FlatPHP
+     */
+    public static function flatten(array $source, string $start = ''): array
+    {
+        $opt = [
+            'prefix'          => '[',
+            'suffix'          => ']',
+            'suffix-end'      => true,
+            'prefix-list'     => '[',
+            'suffix-list'     => ']',
+            'suffix-list-end' => true,
+        ];
+
+        if ($start === '') {
+            $currentPrefix    = '';
+            $currentSuffix    = '';
+            $currentSuffixEnd = false;
+        } elseif (array_is_list($source)) {
+            $currentPrefix    = $opt['prefix-list'];
+            $currentSuffix    = $opt['suffix-list'];
+            $currentSuffixEnd = $opt['suffix-list-end'];
+        } else {
+            $currentPrefix    = $opt['prefix'];
+            $currentSuffix    = $opt['suffix'];
+            $currentSuffixEnd = $opt['suffix-end'];
+        }
+
+        $currentName = $start;
+        $result = [];
+
+        foreach ($source as $key => $val) {
+            $currentName .= $currentPrefix.$key;
+
+            if (is_array($val) && !empty($val)) {
+                $currentName .= $currentSuffix;
+                $result += self::flatten($val, $currentName);
+            } else {
+                if ($currentSuffixEnd) {
+                    $currentName .= $currentSuffix;
+                }
+
+                $result[$currentName] = ObjectSerializer::toString($val);
+            }
+
+            $currentName = $start;
+        }
+
+        return $result;
+    }
+
+    /**
+     * formdata must be limited to scalars or arrays of scalar values,
+     * or a resource for a file upload. Here we iterate through all available
+     * data and identify how to handle each scenario
+     */
+    protected function makeFormSafe($value)
+    {
+        if ($value instanceof SplFileObject) {
+            return $this->processFiles([$value])[0];
+        }
+
+        if (is_resource($value)) {
+            $this->has_file = true;
+
+            return $value;
+        }
+
+        if ($value instanceof ModelInterface) {
+            return $this->processModel($value);
+        }
+
+        if (is_array($value) || is_object($value)) {
+            $data = [];
+
+            foreach ($value as $k => $v) {
+                $data[$k] = $this->makeFormSafe($v);
+            }
+
+            return $data;
+        }
+
+        return ObjectSerializer::toString($value);
+    }
+
+    /**
+     * We are able to handle nested ModelInterface. We do not simply call
+     * json_decode(json_encode()) because any given model may have binary data
+     * or other data that cannot be serialized to a JSON string
+     */
+    protected function processModel(ModelInterface $model): array
+    {
+        $result = [];
+
+        foreach ($model::openAPITypes() as $name => $type) {
+            $value = $model->offsetGet($name);
+
+            if ($value === null) {
+                continue;
+            }
+
+            if (strpos($type, '\SplFileObject') !== false) {
+                $file = is_array($value) ? $value : [$value];
+                $result[$name] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($value instanceof ModelInterface) {
+                $result[$name] = $this->processModel($value);
+
+                continue;
+            }
+
+            if (is_array($value) || is_object($value)) {
+                $result[$name] = $this->makeFormSafe($value);
+
+                continue;
+            }
+
+            $result[$name] = ObjectSerializer::toString($value);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Handle file data
+     */
+    protected function processFiles(array $files): array
+    {
+        $this->has_file = true;
+
+        $result = [];
+
+        foreach ($files as $i => $file) {
+            if (is_array($file)) {
+                $result[$i] = $this->processFiles($file);
+
+                continue;
+            }
+
+            if ($file instanceof StreamInterface) {
+                $result[$i] = $file;
+
+                continue;
+            }
+
+            if ($file instanceof SplFileObject) {
+                $result[$i] = $this->tryFopen($file);
+            }
+        }
+
+        return $result;
+    }
+
+    private function tryFopen(SplFileObject $file)
+    {
+        return Utils::tryFopen($file->getRealPath(), 'rb');
+    }
+}
diff --git a/samples/client/petstore/php/psr-18/lib/Model/PetWithFile.php b/samples/client/petstore/php/psr-18/lib/Model/PetWithFile.php
new file mode 100644
index 000000000000..bc74a7add1e5
--- /dev/null
+++ b/samples/client/petstore/php/psr-18/lib/Model/PetWithFile.php
@@ -0,0 +1,691 @@
+<?php
+/**
+ * PetWithFile
+ *
+ * PHP version 7.4
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * OpenAPI Petstore
+ *
+ * This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.13.0-SNAPSHOT
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace OpenAPI\Client\Model;
+
+use \ArrayAccess;
+use \OpenAPI\Client\ObjectSerializer;
+
+/**
+ * PetWithFile Class Doc Comment
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PetWithFile implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+      * The original name of the model.
+      *
+      * @var string
+      */
+    protected static $openAPIModelName = 'PetWithFile';
+
+    /**
+      * Array of property to type mappings. Used for (de)serialization
+      *
+      * @var string[]
+      */
+    protected static $openAPITypes = [
+        'id' => 'int',
+        'category' => '\OpenAPI\Client\Model\Category',
+        'name' => 'string',
+        'photo_urls' => 'string[]',
+        'tags' => '\OpenAPI\Client\Model\Tag[]',
+        'status' => 'string',
+        'file' => '\SplFileObject',
+        'multiple_files' => '\SplFileObject[]'
+    ];
+
+    /**
+      * Array of property to format mappings. Used for (de)serialization
+      *
+      * @var string[]
+      * @phpstan-var array<string, string|null>
+      * @psalm-var array<string, string|null>
+      */
+    protected static $openAPIFormats = [
+        'id' => 'int64',
+        'category' => null,
+        'name' => null,
+        'photo_urls' => null,
+        'tags' => null,
+        'status' => null,
+        'file' => 'binary',
+        'multiple_files' => 'binary'
+    ];
+
+    /**
+      * Array of nullable properties. Used for (de)serialization
+      *
+      * @var boolean[]
+      */
+    protected static array $openAPINullables = [
+        'id' => false,
+        'category' => false,
+        'name' => false,
+        'photo_urls' => false,
+        'tags' => false,
+        'status' => false,
+        'file' => false,
+        'multiple_files' => false
+    ];
+
+    /**
+      * If a nullable field gets set to null, insert it here
+      *
+      * @var boolean[]
+      */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'id' => 'id',
+        'category' => 'category',
+        'name' => 'name',
+        'photo_urls' => 'photoUrls',
+        'tags' => 'tags',
+        'status' => 'status',
+        'file' => 'file',
+        'multiple_files' => 'multiple_files'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'id' => 'setId',
+        'category' => 'setCategory',
+        'name' => 'setName',
+        'photo_urls' => 'setPhotoUrls',
+        'tags' => 'setTags',
+        'status' => 'setStatus',
+        'file' => 'setFile',
+        'multiple_files' => 'setMultipleFiles'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'id' => 'getId',
+        'category' => 'getCategory',
+        'name' => 'getName',
+        'photo_urls' => 'getPhotoUrls',
+        'tags' => 'getTags',
+        'status' => 'getStatus',
+        'file' => 'getFile',
+        'multiple_files' => 'getMultipleFiles'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+    public const STATUS_AVAILABLE = 'available';
+    public const STATUS_PENDING = 'pending';
+    public const STATUS_SOLD = 'sold';
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getStatusAllowableValues()
+    {
+        return [
+            self::STATUS_AVAILABLE,
+            self::STATUS_PENDING,
+            self::STATUS_SOLD,
+        ];
+    }
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('id', $data ?? [], null);
+        $this->setIfExists('category', $data ?? [], null);
+        $this->setIfExists('name', $data ?? [], null);
+        $this->setIfExists('photo_urls', $data ?? [], null);
+        $this->setIfExists('tags', $data ?? [], null);
+        $this->setIfExists('status', $data ?? [], null);
+        $this->setIfExists('file', $data ?? [], null);
+        $this->setIfExists('multiple_files', $data ?? [], null);
+    }
+
+    /**
+    * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+    * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+    * $this->openAPINullablesSetToNull array
+    *
+    * @param string $variableName
+    * @param array  $fields
+    * @param mixed  $defaultValue
+    */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        if ($this->container['name'] === null) {
+            $invalidProperties[] = "'name' can't be null";
+        }
+        if ($this->container['photo_urls'] === null) {
+            $invalidProperties[] = "'photo_urls' can't be null";
+        }
+        $allowedValues = $this->getStatusAllowableValues();
+        if (!is_null($this->container['status']) && !in_array($this->container['status'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'status', must be one of '%s'",
+                $this->container['status'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets id
+     *
+     * @return int|null
+     */
+    public function getId()
+    {
+        return $this->container['id'];
+    }
+
+    /**
+     * Sets id
+     *
+     * @param int|null $id id
+     *
+     * @return self
+     */
+    public function setId($id)
+    {
+        if (is_null($id)) {
+            throw new \InvalidArgumentException('non-nullable id cannot be null');
+        }
+        $this->container['id'] = $id;
+
+        return $this;
+    }
+
+    /**
+     * Gets category
+     *
+     * @return \OpenAPI\Client\Model\Category|null
+     */
+    public function getCategory()
+    {
+        return $this->container['category'];
+    }
+
+    /**
+     * Sets category
+     *
+     * @param \OpenAPI\Client\Model\Category|null $category category
+     *
+     * @return self
+     */
+    public function setCategory($category)
+    {
+        if (is_null($category)) {
+            throw new \InvalidArgumentException('non-nullable category cannot be null');
+        }
+        $this->container['category'] = $category;
+
+        return $this;
+    }
+
+    /**
+     * Gets name
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->container['name'];
+    }
+
+    /**
+     * Sets name
+     *
+     * @param string $name name
+     *
+     * @return self
+     */
+    public function setName($name)
+    {
+        if (is_null($name)) {
+            throw new \InvalidArgumentException('non-nullable name cannot be null');
+        }
+        $this->container['name'] = $name;
+
+        return $this;
+    }
+
+    /**
+     * Gets photo_urls
+     *
+     * @return string[]
+     */
+    public function getPhotoUrls()
+    {
+        return $this->container['photo_urls'];
+    }
+
+    /**
+     * Sets photo_urls
+     *
+     * @param string[] $photo_urls photo_urls
+     *
+     * @return self
+     */
+    public function setPhotoUrls($photo_urls)
+    {
+        if (is_null($photo_urls)) {
+            throw new \InvalidArgumentException('non-nullable photo_urls cannot be null');
+        }
+
+
+        $this->container['photo_urls'] = $photo_urls;
+
+        return $this;
+    }
+
+    /**
+     * Gets tags
+     *
+     * @return \OpenAPI\Client\Model\Tag[]|null
+     */
+    public function getTags()
+    {
+        return $this->container['tags'];
+    }
+
+    /**
+     * Sets tags
+     *
+     * @param \OpenAPI\Client\Model\Tag[]|null $tags tags
+     *
+     * @return self
+     */
+    public function setTags($tags)
+    {
+        if (is_null($tags)) {
+            throw new \InvalidArgumentException('non-nullable tags cannot be null');
+        }
+        $this->container['tags'] = $tags;
+
+        return $this;
+    }
+
+    /**
+     * Gets status
+     *
+     * @return string|null
+     */
+    public function getStatus()
+    {
+        return $this->container['status'];
+    }
+
+    /**
+     * Sets status
+     *
+     * @param string|null $status pet status in the store
+     *
+     * @return self
+     */
+    public function setStatus($status)
+    {
+        if (is_null($status)) {
+            throw new \InvalidArgumentException('non-nullable status cannot be null');
+        }
+        $allowedValues = $this->getStatusAllowableValues();
+        if (!in_array($status, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'status', must be one of '%s'",
+                    $status,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['status'] = $status;
+
+        return $this;
+    }
+
+    /**
+     * Gets file
+     *
+     * @return \SplFileObject|null
+     */
+    public function getFile()
+    {
+        return $this->container['file'];
+    }
+
+    /**
+     * Sets file
+     *
+     * @param \SplFileObject|null $file file to upload
+     *
+     * @return self
+     */
+    public function setFile($file)
+    {
+        if (is_null($file)) {
+            throw new \InvalidArgumentException('non-nullable file cannot be null');
+        }
+        $this->container['file'] = $file;
+
+        return $this;
+    }
+
+    /**
+     * Gets multiple_files
+     *
+     * @return \SplFileObject[]|null
+     */
+    public function getMultipleFiles()
+    {
+        return $this->container['multiple_files'];
+    }
+
+    /**
+     * Sets multiple_files
+     *
+     * @param \SplFileObject[]|null $multiple_files multiple_files
+     *
+     * @return self
+     */
+    public function setMultipleFiles($multiple_files)
+    {
+        if (is_null($multiple_files)) {
+            throw new \InvalidArgumentException('non-nullable multiple_files cannot be null');
+        }
+        $this->container['multiple_files'] = $multiple_files;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists($offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet($offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset($offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+
diff --git a/samples/client/petstore/php/psr-18/test/Model/PetWithFileTest.php b/samples/client/petstore/php/psr-18/test/Model/PetWithFileTest.php
new file mode 100644
index 000000000000..ce0a73f294d0
--- /dev/null
+++ b/samples/client/petstore/php/psr-18/test/Model/PetWithFileTest.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * PetWithFileTest
+ *
+ * PHP version 7.4
+ *
+ * @category Class
+ * @package  OpenAPI\Client
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * OpenAPI Petstore
+ *
+ * This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.13.0-SNAPSHOT
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Please update the test case below to test the model.
+ */
+
+namespace OpenAPI\Client\Test\Model;
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * PetWithFileTest Class Doc Comment
+ *
+ * @category    Class
+ * @description PetWithFile
+ * @package     OpenAPI\Client
+ * @author      OpenAPI Generator team
+ * @link        https://openapi-generator.tech
+ */
+class PetWithFileTest extends TestCase
+{
+
+    /**
+     * Setup before running any test case
+     */
+    public static function setUpBeforeClass(): void
+    {
+    }
+
+    /**
+     * Setup before running each test case
+     */
+    public function setUp(): void
+    {
+    }
+
+    /**
+     * Clean up after running each test case
+     */
+    public function tearDown(): void
+    {
+    }
+
+    /**
+     * Clean up after running all test cases
+     */
+    public static function tearDownAfterClass(): void
+    {
+    }
+
+    /**
+     * Test "PetWithFile"
+     */
+    public function testPetWithFile()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "id"
+     */
+    public function testPropertyId()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "category"
+     */
+    public function testPropertyCategory()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "name"
+     */
+    public function testPropertyName()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "photo_urls"
+     */
+    public function testPropertyPhotoUrls()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "tags"
+     */
+    public function testPropertyTags()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "status"
+     */
+    public function testPropertyStatus()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "file"
+     */
+    public function testPropertyFile()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+
+    /**
+     * Test attribute "multiple_files"
+     */
+    public function testPropertyMultipleFiles()
+    {
+        // TODO: implement
+        self::markTestIncomplete('Not implemented');
+    }
+}

From c98db05df3fb4a53fbbd5afc33a011e06d7c54aa Mon Sep 17 00:00:00 2001
From: Juan Treminio <jtreminio@gmail.com>
Date: Fri, 28 Mar 2025 18:54:49 -0500
Subject: [PATCH 05/10] Some more tests

---
 .../tests/FormDataProcessorTest.php           | 55 +++++++++++++++++++
 1 file changed, 55 insertions(+)

diff --git a/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php b/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php
index 501b61101254..02176c99edfa 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php
@@ -111,6 +111,23 @@ public function providerFlatten(): iterable
             ],
         ];
 
+        yield [
+            'data'     => ['nested' => ['pet' => $pet]],
+            'expected' => [
+                'nested[pet][id]'             => $data['id'],
+                'nested[pet][name]'           => $data['name'],
+                'nested[pet][photo_urls][0]'  => $data['photo_urls'][0],
+                'nested[pet][photo_urls][1]'  => $data['photo_urls'][1],
+                'nested[pet][status]'         => $data['status'],
+                'nested[pet][category][id]'   => (string) $data['category']['id'],
+                'nested[pet][category][name]' => $data['category']['name'],
+                'nested[pet][tags][0][id]'    => (string) $data['tags'][0]['id'],
+                'nested[pet][tags][0][name]'  => $data['tags'][0]['name'],
+                'nested[pet][tags][1][id]'    => (string) $data['tags'][1]['id'],
+                'nested[pet][tags][1][name]'  => $data['tags'][1]['name'],
+            ],
+        ];
+
         yield [
             'data'     => ['key' => new DateTime('2021-10-06T20:17:16')],
             'expected' => ['key' => '2021-10-06T20:17:16+00:00'],
@@ -151,6 +168,25 @@ public function testHasFile(): void
         $this->assertTrue($formDataProcessor->has_file);
     }
 
+    public function testHasFileNested(): void
+    {
+        $filepath = realpath(__DIR__ . '/../.openapi-generator/VERSION');
+        $file = new SplFileObject($filepath);
+
+        $pet = new Model\PetWithFile([]);
+        $pet->setId(123)
+            ->setName('Spike')
+            ->setFile($file);
+
+        $formDataProcessor = new FormDataProcessor();
+
+        $this->assertFalse($formDataProcessor->has_file);
+        $formData = $formDataProcessor->prepare(['nested' => ['pet' => $pet]]);
+
+        $this->assertIsResource($formData['nested']['pet']['file'][0]);
+        $this->assertTrue($formDataProcessor->has_file);
+    }
+
     public function testHasFileMultiple(): void
     {
         $filepath = realpath(__DIR__ . '/../.openapi-generator/VERSION');
@@ -169,4 +205,23 @@ public function testHasFileMultiple(): void
         $this->assertIsResource($formData['pet']['multiple_files'][0]);
         $this->assertTrue($formDataProcessor->has_file);
     }
+
+    public function testHasFileMultipleNested(): void
+    {
+        $filepath = realpath(__DIR__ . '/../.openapi-generator/VERSION');
+        $file = new SplFileObject($filepath);
+
+        $pet = new Model\PetWithFile([]);
+        $pet->setId(123)
+            ->setName('Spike')
+            ->setMultipleFiles([$file]);
+
+        $formDataProcessor = new FormDataProcessor();
+
+        $this->assertFalse($formDataProcessor->has_file);
+        $formData = $formDataProcessor->prepare(['nested' => ['pet' => $pet]]);
+
+        $this->assertIsResource($formData['nested']['pet']['multiple_files'][0]);
+        $this->assertTrue($formDataProcessor->has_file);
+    }
 }

From df4daa6b0c876502b808cd91264182ba2b7d5b58 Mon Sep 17 00:00:00 2001
From: Juan Treminio <jtreminio@gmail.com>
Date: Fri, 28 Mar 2025 19:05:54 -0500
Subject: [PATCH 06/10] One last test

---
 .../tests/FormDataProcessorTest.php           | 24 +++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php b/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php
index 02176c99edfa..ea264d594623 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/tests/FormDataProcessorTest.php
@@ -149,6 +149,30 @@ public function providerFlatten(): iterable
         ];
     }
 
+    public function testNullValueIgnored(): void
+    {
+        $data = [
+            'id'         => '1234',
+            'name'       => 'Spike',
+            'photo_urls' => null,
+            'status'     => null,
+            'category'   => null,
+            'tags'       => null,
+        ];
+
+        $expected = [
+            'id'   => $data['id'],
+            'name' => $data['name'],
+        ];
+
+        $formDataProcessor = new FormDataProcessor();
+        $formData = $formDataProcessor->prepare($data);
+
+        $result = $formDataProcessor::flatten($formData);
+
+        $this->assertEquals($expected, $result);
+    }
+
     public function testHasFile(): void
     {
         $filepath = realpath(__DIR__ . '/../.openapi-generator/VERSION');

From 3193487280b28cbb7c6258ed161c4c25e67d877f Mon Sep 17 00:00:00 2001
From: Juan Treminio <jtreminio@gmail.com>
Date: Fri, 28 Mar 2025 19:07:59 -0500
Subject: [PATCH 07/10] Updating files

---
 .../src/main/resources/php/FormDataProcessor.mustache           | 2 +-
 .../petstore/php/OpenAPIClient-php/.openapi-generator/FILES     | 1 -
 samples/client/petstore/php/psr-18/.openapi-generator/FILES     | 1 -
 samples/client/petstore/php/psr-18/lib/FormDataProcessor.php    | 2 +-
 4 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache b/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache
index 89751edb321a..6985fd6c573f 100644
--- a/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache
+++ b/modules/openapi-generator/src/main/resources/php/FormDataProcessor.mustache
@@ -142,7 +142,7 @@ class FormDataProcessor
             return $this->processModel($value);
         }
 
-        if (is_array($value) || is_object($value)) {
+        if (is_array($value) || (is_object($value) && !$value instanceof \DateTimeInterface)) {
             $data = [];
 
             foreach ($value as $k => $v) {
diff --git a/samples/client/petstore/php/OpenAPIClient-php/.openapi-generator/FILES b/samples/client/petstore/php/OpenAPIClient-php/.openapi-generator/FILES
index b8f153bf7f98..a7ab9665e70e 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/.openapi-generator/FILES
+++ b/samples/client/petstore/php/OpenAPIClient-php/.openapi-generator/FILES
@@ -127,4 +127,3 @@ lib/Model/TestInlineFreeformAdditionalPropertiesRequest.php
 lib/Model/User.php
 lib/ObjectSerializer.php
 phpunit.xml.dist
-test/Model/PetWithFileTest.php
diff --git a/samples/client/petstore/php/psr-18/.openapi-generator/FILES b/samples/client/petstore/php/psr-18/.openapi-generator/FILES
index 404c4d9c109c..3f5736b9a351 100644
--- a/samples/client/petstore/php/psr-18/.openapi-generator/FILES
+++ b/samples/client/petstore/php/psr-18/.openapi-generator/FILES
@@ -128,4 +128,3 @@ lib/Model/TestInlineFreeformAdditionalPropertiesRequest.php
 lib/Model/User.php
 lib/ObjectSerializer.php
 phpunit.xml.dist
-test/Model/PetWithFileTest.php
diff --git a/samples/client/petstore/php/psr-18/lib/FormDataProcessor.php b/samples/client/petstore/php/psr-18/lib/FormDataProcessor.php
index 34a446123bcf..c416f584e6aa 100644
--- a/samples/client/petstore/php/psr-18/lib/FormDataProcessor.php
+++ b/samples/client/petstore/php/psr-18/lib/FormDataProcessor.php
@@ -151,7 +151,7 @@ protected function makeFormSafe($value)
             return $this->processModel($value);
         }
 
-        if (is_array($value) || is_object($value)) {
+        if (is_array($value) || (is_object($value) && !$value instanceof \DateTimeInterface)) {
             $data = [];
 
             foreach ($value as $k => $v) {

From 55f07b04a822acb5b7c990d7a04e32aac5db2f62 Mon Sep 17 00:00:00 2001
From: Juan Treminio <jtreminio@gmail.com>
Date: Fri, 28 Mar 2025 19:16:04 -0500
Subject: [PATCH 08/10] Fixing diff

---
 .../petstore/php/OpenAPIClient-php/lib/Model/PetWithFile.php    | 2 +-
 samples/client/petstore/php/psr-18/lib/Model/PetWithFile.php    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Model/PetWithFile.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Model/PetWithFile.php
index bc74a7add1e5..3e91dabb4b55 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/Model/PetWithFile.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Model/PetWithFile.php
@@ -2,7 +2,7 @@
 /**
  * PetWithFile
  *
- * PHP version 7.4
+ * PHP version 8.1
  *
  * @category Class
  * @package  OpenAPI\Client
diff --git a/samples/client/petstore/php/psr-18/lib/Model/PetWithFile.php b/samples/client/petstore/php/psr-18/lib/Model/PetWithFile.php
index bc74a7add1e5..3e91dabb4b55 100644
--- a/samples/client/petstore/php/psr-18/lib/Model/PetWithFile.php
+++ b/samples/client/petstore/php/psr-18/lib/Model/PetWithFile.php
@@ -2,7 +2,7 @@
 /**
  * PetWithFile
  *
- * PHP version 7.4
+ * PHP version 8.1
  *
  * @category Class
  * @package  OpenAPI\Client

From 363073e2caec5877980f885f3bd52c14398d11b6 Mon Sep 17 00:00:00 2001
From: Juan Treminio <jtreminio@gmail.com>
Date: Tue, 15 Apr 2025 10:15:33 -0500
Subject: [PATCH 09/10] Test fix

---
 .../petstore/php/OpenAPIClient-php/tests/PetApiTest.php       | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/samples/client/petstore/php/OpenAPIClient-php/tests/PetApiTest.php b/samples/client/petstore/php/OpenAPIClient-php/tests/PetApiTest.php
index 463f36a78af0..bd1c2fb2a8a9 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/tests/PetApiTest.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/tests/PetApiTest.php
@@ -417,8 +417,8 @@ public function testObjectInFormData()
         $contents = $request->getBody()->getContents();
 
         $this->assertBodyContents('name', $name, $contents);
-        $this->assertBodyContents('photoUrls[0]', $photo_urls[0], $contents);
-        $this->assertBodyContents('photoUrls[1]', $photo_urls[1], $contents);
+        $this->assertBodyContents('photo_urls[0]', $photo_urls[0], $contents);
+        $this->assertBodyContents('photo_urls[1]', $photo_urls[1], $contents);
         $this->assertBodyContents('category[id]', $category->getId(), $contents);
         $this->assertBodyContents('category[name]', $category->getName(), $contents);
         $this->assertBodyContents('tags[0][id]', $tags[0]->getId(), $contents);

From 4453d72a649f6bc16b310d1835a780b78cecaab7 Mon Sep 17 00:00:00 2001
From: Juan Treminio <jtreminio@gmail.com>
Date: Tue, 15 Apr 2025 10:24:53 -0500
Subject: [PATCH 10/10] Updating samples

---
 .../php/OpenAPIClient-php/lib/Api/PetApi.php  | 70 ++++---------------
 .../petstore/php/psr-18/lib/Api/PetApi.php    | 50 +++++++------
 2 files changed, 42 insertions(+), 78 deletions(-)

diff --git a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php
index 010706f7d99a..61f60642dadf 100644
--- a/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php
+++ b/samples/client/petstore/php/OpenAPIClient-php/lib/Api/PetApi.php
@@ -3189,34 +3189,15 @@ public function uploadImageFullFormDataNestedWithHttpInfo($pet_id, $pet = null,
 
             switch($statusCode) {
                 case 200:
-                    if ('\OpenAPI\Client\Model\ApiResponse' === '\SplFileObject') {
-                        $content = $response->getBody(); //stream goes to serializer
-                    } else {
-                        $content = (string) $response->getBody();
-                        if ('\OpenAPI\Client\Model\ApiResponse' !== 'string') {
-                            try {
-                                $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR);
-                            } catch (\JsonException $exception) {
-                                throw new ApiException(
-                                    sprintf(
-                                        'Error JSON decoding server response (%s)',
-                                        $request->getUri()
-                                    ),
-                                    $statusCode,
-                                    $response->getHeaders(),
-                                    $content
-                                );
-                            }
-                        }
-                    }
-
-                    return [
-                        ObjectSerializer::deserialize($content, '\OpenAPI\Client\Model\ApiResponse', []),
-                        $response->getStatusCode(),
-                        $response->getHeaders()
-                    ];
+                    return $this->handleResponseWithDataType(
+                        '\OpenAPI\Client\Model\ApiResponse',
+                        $request,
+                        $response,
+                    );
             }
 
+            
+
             if ($statusCode < 200 || $statusCode > 299) {
                 throw new ApiException(
                     sprintf(
@@ -3230,34 +3211,11 @@ public function uploadImageFullFormDataNestedWithHttpInfo($pet_id, $pet = null,
                 );
             }
 
-            $returnType = '\OpenAPI\Client\Model\ApiResponse';
-            if ($returnType === '\SplFileObject') {
-                $content = $response->getBody(); //stream goes to serializer
-            } else {
-                $content = (string) $response->getBody();
-                if ($returnType !== 'string') {
-                    try {
-                        $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR);
-                    } catch (\JsonException $exception) {
-                        throw new ApiException(
-                            sprintf(
-                                'Error JSON decoding server response (%s)',
-                                $request->getUri()
-                            ),
-                            $statusCode,
-                            $response->getHeaders(),
-                            $content
-                        );
-                    }
-                }
-            }
-
-            return [
-                ObjectSerializer::deserialize($content, $returnType, []),
-                $response->getStatusCode(),
-                $response->getHeaders()
-            ];
-
+            return $this->handleResponseWithDataType(
+                '\OpenAPI\Client\Model\ApiResponse',
+                $request,
+                $response,
+            );
         } catch (ApiException $e) {
             switch ($e->getCode()) {
                 case 200:
@@ -3267,8 +3225,10 @@ public function uploadImageFullFormDataNestedWithHttpInfo($pet_id, $pet = null,
                         $e->getResponseHeaders()
                     );
                     $e->setResponseObject($data);
-                    break;
+                    throw $e;
             }
+        
+
             throw $e;
         }
     }
diff --git a/samples/client/petstore/php/psr-18/lib/Api/PetApi.php b/samples/client/petstore/php/psr-18/lib/Api/PetApi.php
index bc7ebcebef56..8f623f5eb0f2 100644
--- a/samples/client/petstore/php/psr-18/lib/Api/PetApi.php
+++ b/samples/client/petstore/php/psr-18/lib/Api/PetApi.php
@@ -2864,34 +2864,36 @@ public function uploadImageFullFormDataNestedWithHttpInfo($pet_id, $pet = null)
 
             $statusCode = $response->getStatusCode();
 
+
             switch($statusCode) {
                 case 200:
-                    if ('\OpenAPI\Client\Model\ApiResponse' === '\SplFileObject') {
-                        $content = $response->getBody(); //stream goes to serializer
-                    } else {
-                        $content = (string) $response->getBody();
-                    }
-
-                    return [
-                        ObjectSerializer::deserialize($content, '\OpenAPI\Client\Model\ApiResponse', []),
-                        $response->getStatusCode(),
-                        $response->getHeaders()
-                    ];
+                    return $this->handleResponseWithDataType(
+                        '\OpenAPI\Client\Model\ApiResponse',
+                        $request,
+                        $response,
+                    );
             }
 
-            $returnType = '\OpenAPI\Client\Model\ApiResponse';
-            if ($returnType === '\SplFileObject') {
-                $content = $response->getBody(); //stream goes to serializer
-            } else {
-                $content = (string) $response->getBody();
-            }
+            
 
-            return [
-                ObjectSerializer::deserialize($content, $returnType, []),
-                $response->getStatusCode(),
-                $response->getHeaders()
-            ];
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
 
+            return $this->handleResponseWithDataType(
+                '\OpenAPI\Client\Model\ApiResponse',
+                $request,
+                $response,
+            );
         } catch (ApiException $e) {
             switch ($e->getCode()) {
                 case 200:
@@ -2901,8 +2903,10 @@ public function uploadImageFullFormDataNestedWithHttpInfo($pet_id, $pet = null)
                         $e->getResponseHeaders()
                     );
                     $e->setResponseObject($data);
-                    break;
+                    throw $e;
             }
+        
+
             throw $e;
         }
     }