Skip to content

Commit 3462310

Browse files
authored
[PHP] - Add FormDataProcessor to handle nested ModelInterface data (OpenAPITools#20990)
* [PHP] - Add FormDataProcessor to handle nested ModelInterface data * Generating samples * Updates php-nextgen and psr-18 * Adds tests * Some more tests * One last test * Updating files * Fixing diff * Test fix * Updating samples
1 parent b844d8d commit 3462310

File tree

71 files changed

+4747
-1363
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+4747
-1363
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpClientCodegen.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public void processOpts() {
117117

118118
supportingFiles.add(new SupportingFile("ApiException.mustache", toSrcPath(invokerPackage, srcBasePath), "ApiException.php"));
119119
supportingFiles.add(new SupportingFile("Configuration.mustache", toSrcPath(invokerPackage, srcBasePath), "Configuration.php"));
120+
supportingFiles.add(new SupportingFile("FormDataProcessor.mustache", toSrcPath(invokerPackage, srcBasePath), "FormDataProcessor.php"));
120121
supportingFiles.add(new SupportingFile("ObjectSerializer.mustache", toSrcPath(invokerPackage, srcBasePath), "ObjectSerializer.php"));
121122
supportingFiles.add(new SupportingFile("ModelInterface.mustache", toSrcPath(modelPackage, srcBasePath), "ModelInterface.php"));
122123
supportingFiles.add(new SupportingFile("HeaderSelector.mustache", toSrcPath(invokerPackage, srcBasePath), "HeaderSelector.php"));

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpNextgenClientCodegen.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public void processOpts() {
121121

122122
supportingFiles.add(new SupportingFile("ApiException.mustache", toSrcPath(invokerPackage, srcBasePath), "ApiException.php"));
123123
supportingFiles.add(new SupportingFile("Configuration.mustache", toSrcPath(invokerPackage, srcBasePath), "Configuration.php"));
124+
supportingFiles.add(new SupportingFile("FormDataProcessor.mustache", toSrcPath(invokerPackage, srcBasePath), "FormDataProcessor.php"));
124125
supportingFiles.add(new SupportingFile("ObjectSerializer.mustache", toSrcPath(invokerPackage, srcBasePath), "ObjectSerializer.php"));
125126
supportingFiles.add(new SupportingFile("ModelInterface.mustache", toSrcPath(modelPackage, srcBasePath), "ModelInterface.php"));
126127
supportingFiles.add(new SupportingFile("HeaderSelector.mustache", toSrcPath(invokerPackage, srcBasePath), "HeaderSelector.php"));
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
<?php
2+
/**
3+
* FormDataProcessor
4+
* PHP version 8.1
5+
*
6+
* @category Class
7+
* @package {{invokerPackage}}
8+
* @author OpenAPI Generator team
9+
* @link https://openapi-generator.tech
10+
*/
11+
12+
{{>partial_header}}
13+
/**
14+
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
15+
* https://openapi-generator.tech
16+
* Do not edit the class manually.
17+
*/
18+
19+
namespace {{invokerPackage}};
20+
21+
use ArrayAccess;
22+
use DateTime;
23+
use GuzzleHttp\Psr7\Utils;
24+
use Psr\Http\Message\StreamInterface;
25+
use SplFileObject;
26+
use {{modelPackage}}\ModelInterface;
27+
28+
class FormDataProcessor
29+
{
30+
/**
31+
* Tags whether payload passed to ::prepare() contains one or more
32+
* SplFileObject or stream values.
33+
*/
34+
public bool $has_file = false;
35+
36+
/**
37+
* Take value and turn it into an array suitable for inclusion in
38+
* the http body (form parameter). If it's a string, pass through unchanged
39+
* If it's a datetime object, format it in ISO8601
40+
*
41+
* @param array<string|bool|array|DateTime|ArrayAccess|SplFileObject> $values the value of the form parameter
42+
*
43+
* @return array [key => value] of formdata
44+
*/
45+
public function prepare(array $values): array
46+
{
47+
$this->has_file = false;
48+
$result = [];
49+
50+
foreach ($values as $k => $v) {
51+
if ($v === null) {
52+
continue;
53+
}
54+
55+
$result[$k] = $this->makeFormSafe($v);
56+
}
57+
58+
return $result;
59+
}
60+
61+
/**
62+
* Flattens a multi-level array of data and generates a single-level array
63+
* compatible with formdata - a single-level array where the keys use bracket
64+
* notation to signify nested data.
65+
*
66+
* credit: https://github.com/FranBar1966/FlatPHP
67+
*/
68+
public static function flatten(array $source, string $start = ''): array
69+
{
70+
$opt = [
71+
'prefix' => '[',
72+
'suffix' => ']',
73+
'suffix-end' => true,
74+
'prefix-list' => '[',
75+
'suffix-list' => ']',
76+
'suffix-list-end' => true,
77+
];
78+
79+
if ($start === '') {
80+
$currentPrefix = '';
81+
$currentSuffix = '';
82+
$currentSuffixEnd = false;
83+
} elseif (array_is_list($source)) {
84+
$currentPrefix = $opt['prefix-list'];
85+
$currentSuffix = $opt['suffix-list'];
86+
$currentSuffixEnd = $opt['suffix-list-end'];
87+
} else {
88+
$currentPrefix = $opt['prefix'];
89+
$currentSuffix = $opt['suffix'];
90+
$currentSuffixEnd = $opt['suffix-end'];
91+
}
92+
93+
$currentName = $start;
94+
$result = [];
95+
96+
foreach ($source as $key => $val) {
97+
$currentName .= $currentPrefix . $key;
98+
99+
if (is_array($val) && !empty($val)) {
100+
$currentName .= $currentSuffix;
101+
$result += self::flatten($val, $currentName);
102+
} else {
103+
if ($currentSuffixEnd) {
104+
$currentName .= $currentSuffix;
105+
}
106+
107+
$result[$currentName] = ObjectSerializer::toString($val);
108+
}
109+
110+
$currentName = $start;
111+
}
112+
113+
return $result;
114+
}
115+
116+
/**
117+
* formdata must be limited to scalars or arrays of scalar values,
118+
* or a resource for a file upload. Here we iterate through all available
119+
* data and identify how to handle each scenario
120+
*
121+
* @param string|bool|array|DateTime|ArrayAccess|SplFileObject $value
122+
*/
123+
protected function makeFormSafe(mixed $value)
124+
{
125+
if ($value instanceof SplFileObject) {
126+
return $this->processFiles([$value])[0];
127+
}
128+
129+
if (is_resource($value)) {
130+
$this->has_file = true;
131+
132+
return $value;
133+
}
134+
135+
if ($value instanceof ModelInterface) {
136+
return $this->processModel($value);
137+
}
138+
139+
if (is_array($value) || is_object($value)) {
140+
$data = [];
141+
142+
foreach ($value as $k => $v) {
143+
$data[$k] = $this->makeFormSafe($v);
144+
}
145+
146+
return $data;
147+
}
148+
149+
return ObjectSerializer::toString($value);
150+
}
151+
152+
/**
153+
* We are able to handle nested ModelInterface. We do not simply call
154+
* json_decode(json_encode()) because any given model may have binary data
155+
* or other data that cannot be serialized to a JSON string
156+
*/
157+
protected function processModel(ModelInterface $model): array
158+
{
159+
$result = [];
160+
161+
foreach ($model::openAPITypes() as $name => $type) {
162+
$value = $model->offsetGet($name);
163+
164+
if ($value === null) {
165+
continue;
166+
}
167+
168+
if (str_contains($type, '\SplFileObject')) {
169+
$file = is_array($value) ? $value : [$value];
170+
$result[$name] = $this->processFiles($file);
171+
172+
continue;
173+
}
174+
175+
if ($value instanceof ModelInterface) {
176+
$result[$name] = $this->processModel($value);
177+
178+
continue;
179+
}
180+
181+
if (is_array($value) || is_object($value)) {
182+
$result[$name] = $this->makeFormSafe($value);
183+
184+
continue;
185+
}
186+
187+
$result[$name] = ObjectSerializer::toString($value);
188+
}
189+
190+
return $result;
191+
}
192+
193+
/**
194+
* Handle file data
195+
*/
196+
protected function processFiles(array $files): array
197+
{
198+
$this->has_file = true;
199+
200+
$result = [];
201+
202+
foreach ($files as $i => $file) {
203+
if (is_array($file)) {
204+
$result[$i] = $this->processFiles($file);
205+
206+
continue;
207+
}
208+
209+
if ($file instanceof StreamInterface) {
210+
$result[$i] = $file;
211+
212+
continue;
213+
}
214+
215+
if ($file instanceof SplFileObject) {
216+
$result[$i] = $this->tryFopen($file);
217+
}
218+
}
219+
220+
return $result;
221+
}
222+
223+
private function tryFopen(SplFileObject $file)
224+
{
225+
return Utils::tryFopen($file->getRealPath(), 'rb');
226+
}
227+
}

modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
namespace {{invokerPackage}};
2020

21-
use ArrayAccess;
2221
use DateTimeInterface;
2322
use DateTime;
2423
use GuzzleHttp\Psr7\Utils;
@@ -316,37 +315,6 @@ class ObjectSerializer
316315
return self::toString($value);
317316
}
318317
319-
/**
320-
* Take value and turn it into an array suitable for inclusion in
321-
* the http body (form parameter). If it's a string, pass through unchanged
322-
* If it's a datetime object, format it in ISO8601
323-
*
324-
* @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter
325-
*
326-
* @return array [key => value] of formdata
327-
*/
328-
public static function toFormValue(
329-
string $key,
330-
string|bool|array|DateTime|ArrayAccess|\SplFileObject $value,
331-
): array {
332-
if ($value instanceof \SplFileObject) {
333-
return [$key => $value->getRealPath()];
334-
} elseif (is_array($value) || $value instanceof ArrayAccess) {
335-
$flattened = [];
336-
$result = [];
337-
338-
self::flattenArray(json_decode(json_encode($value), true), $flattened);
339-
340-
foreach ($flattened as $k => $v) {
341-
$result["{$key}{$k}"] = self::toString($v);
342-
}
343-
344-
return $result;
345-
} else {
346-
return [$key => self::toString($value)];
347-
}
348-
}
349-
350318
/**
351319
* Take value and turn it into a string suitable for inclusion in
352320
* the parameter. If it's a string, pass through unchanged
@@ -612,58 +580,4 @@ class ObjectSerializer
612580

613581
return $qs ? (string) substr($qs, 0, -1) : '';
614582
}
615-
616-
/**
617-
* Flattens an array of Model object and generates an array compatible
618-
* with formdata - a single-level array where the keys use bracket
619-
* notation to signify nested data.
620-
*
621-
* credit: https://github.com/FranBar1966/FlatPHP
622-
*/
623-
private static function flattenArray(
624-
ArrayAccess|array $source,
625-
array &$destination,
626-
string $start = '',
627-
) {
628-
$opt = [
629-
'prefix' => '[',
630-
'suffix' => ']',
631-
'suffix-end' => true,
632-
'prefix-list' => '[',
633-
'suffix-list' => ']',
634-
'suffix-list-end' => true,
635-
];
636-
637-
if (!is_array($source)) {
638-
$source = (array) $source;
639-
}
640-
641-
if (array_is_list($source)) {
642-
$currentPrefix = $opt['prefix-list'];
643-
$currentSuffix = $opt['suffix-list'];
644-
$currentSuffixEnd = $opt['suffix-list-end'];
645-
} else {
646-
$currentPrefix = $opt['prefix'];
647-
$currentSuffix = $opt['suffix'];
648-
$currentSuffixEnd = $opt['suffix-end'];
649-
}
650-
651-
$currentName = $start;
652-
653-
foreach ($source as $key => $val) {
654-
$currentName .= $currentPrefix.$key;
655-
656-
if (is_array($val) && !empty($val)) {
657-
$currentName .= "{$currentSuffix}";
658-
self::flattenArray($val, $destination, $currentName);
659-
} else {
660-
if ($currentSuffixEnd) {
661-
$currentName .= $currentSuffix;
662-
}
663-
$destination[$currentName] = self::toString($val);
664-
}
665-
666-
$currentName = $start;
667-
}
668-
}
669583
}

modules/openapi-generator/src/main/resources/php-nextgen/api.mustache

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use Psr\Http\Message\ResponseInterface;
3131
use {{invokerPackage}}\ApiException;
3232
use {{invokerPackage}}\Configuration;
3333
use {{invokerPackage}}\HeaderSelector;
34+
use {{invokerPackage}}\FormDataProcessor;
3435
use {{invokerPackage}}\ObjectSerializer;
3536

3637
/**
@@ -724,25 +725,19 @@ use {{invokerPackage}}\ObjectSerializer;
724725
{{/pathParams}}
725726

726727
{{#formParams}}
728+
{{#-first}}
727729
// form params
728-
if (${{paramName}} !== null) {
729-
{{#isFile}}
730-
$multipart = true;
731-
$formParams['{{baseName}}'] = [];
732-
$paramFiles = is_array(${{paramName}}) ? ${{paramName}} : [${{paramName}}];
733-
foreach ($paramFiles as $paramFile) {
734-
$formParams['{{baseName}}'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
735-
? $paramFile
736-
: \GuzzleHttp\Psr7\Utils::tryFopen(
737-
ObjectSerializer::toFormValue('{{baseName}}', $paramFile)['{{baseName}}'],
738-
'rb'
739-
);
740-
}
741-
{{/isFile}}
742-
{{^isFile}}
743-
$formParams = array_merge($formParams, ObjectSerializer::toFormValue('{{baseName}}', ${{paramName}}));
744-
{{/isFile}}
745-
}
730+
$formDataProcessor = new FormDataProcessor();
731+
732+
$formData = $formDataProcessor->prepare([
733+
{{/-first}}
734+
'{{paramName}}' => ${{paramName}},
735+
{{#-last}}
736+
]);
737+
738+
$formParams = $formDataProcessor->flatten($formData);
739+
$multipart = $formDataProcessor->has_file;
740+
{{/-last}}
746741
{{/formParams}}
747742

748743
$headers = $this->headerSelector->selectHeaders(

0 commit comments

Comments
 (0)