Skip to content

Commit 8f24df4

Browse files
authored
[PHP] Fix converting objects to formdata (#20888)
* Output of CLI commands per PR comments * Rebuilding PHP examples, PSR-18 * Rebuilding PHP examples * Adds explanation for ::flatten_array(); optimized array_is_list pollyfill * [PHP] Fix converting objects to formdata * flatten_array -> flattenArray to match code style * Adds unit test * Revert "Output of CLI commands per PR comments" This reverts commit 2eaa937. * Includes php-nextgen; tightens up ::toFormValue() * Missing ArrayAccess import * Adds test for refactored ObjectSerializer::toFormValue()
1 parent 8a8bacd commit 8f24df4

File tree

27 files changed

+1877
-151
lines changed

27 files changed

+1877
-151
lines changed

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

+75-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
namespace {{invokerPackage}};
2020

21+
use ArrayAccess;
2122
use DateTimeInterface;
2223
use DateTime;
2324
use GuzzleHttp\Psr7\Utils;
@@ -316,20 +317,33 @@ class ObjectSerializer
316317
}
317318
318319
/**
319-
* Take value and turn it into a string suitable for inclusion in
320+
* Take value and turn it into an array suitable for inclusion in
320321
* the http body (form parameter). If it's a string, pass through unchanged
321322
* If it's a datetime object, format it in ISO8601
322323
*
323-
* @param string|\SplFileObject $value the value of the form parameter
324+
* @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter
324325
*
325-
* @return string the form string
326+
* @return array [key => value] of formdata
326327
*/
327-
public static function toFormValue(string|\SplFileObject $value): string
328-
{
328+
public static function toFormValue(
329+
string $key,
330+
string|bool|array|DateTime|ArrayAccess|\SplFileObject $value,
331+
): array {
329332
if ($value instanceof \SplFileObject) {
330-
return $value->getRealPath();
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;
331345
} else {
332-
return self::toString($value);
346+
return [$key => self::toString($value)];
333347
}
334348
}
335349
@@ -598,4 +612,58 @@ class ObjectSerializer
598612

599613
return $qs ? (string) substr($qs, 0, -1) : '';
600614
}
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+
}
601669
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -759,13 +759,13 @@ use {{invokerPackage}}\ObjectSerializer;
759759
$formParams['{{baseName}}'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
760760
? $paramFile
761761
: \GuzzleHttp\Psr7\Utils::tryFopen(
762-
ObjectSerializer::toFormValue($paramFile),
762+
ObjectSerializer::toFormValue('{{baseName}}', $paramFile)['{{baseName}}'],
763763
'rb'
764764
);
765765
}
766766
{{/isFile}}
767767
{{^isFile}}
768-
$formParams['{{baseName}}'] = ObjectSerializer::toFormValue(${{paramName}});
768+
$formParams = array_merge($formParams, ObjectSerializer::toFormValue('{{baseName}}', ${{paramName}}));
769769
{{/isFile}}
770770
}
771771
{{/formParams}}

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

+95-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
namespace {{invokerPackage}};
2121

22+
use ArrayAccess;
2223
use GuzzleHttp\Psr7\Utils;
2324
use {{modelPackage}}\ModelInterface;
2425

@@ -315,20 +316,31 @@ class ObjectSerializer
315316
}
316317
317318
/**
318-
* Take value and turn it into a string suitable for inclusion in
319+
* Take value and turn it into an array suitable for inclusion in
319320
* the http body (form parameter). If it's a string, pass through unchanged
320321
* If it's a datetime object, format it in ISO8601
321322
*
322-
* @param string|\SplFileObject $value the value of the form parameter
323+
* @param string|bool|array|DateTime|ArrayAccess|\SplFileObject $value the value of the form parameter
323324
*
324-
* @return string the form string
325+
* @return array [key => value] of formdata
325326
*/
326-
public static function toFormValue($value)
327+
public static function toFormValue(string $key, mixed $value)
327328
{
328329
if ($value instanceof \SplFileObject) {
329-
return $value->getRealPath();
330+
return [$key => $value->getRealPath()];
331+
} elseif (is_array($value) || $value instanceof ArrayAccess) {
332+
$flattened = [];
333+
$result = [];
334+
335+
self::flattenArray(json_decode(json_encode($value), true), $flattened);
336+
337+
foreach ($flattened as $k => $v) {
338+
$result["{$key}{$k}"] = self::toString($v);
339+
}
340+
341+
return $result;
330342
} else {
331-
return self::toString($value);
343+
return [$key => self::toString($value)];
332344
}
333345
}
334346
@@ -605,4 +617,81 @@ class ObjectSerializer
605617

606618
return $qs ? (string) substr($qs, 0, -1) : '';
607619
}
620+
621+
/**
622+
* Flattens an array of Model object and generates an array compatible
623+
* with formdata - a single-level array where the keys use bracket
624+
* notation to signify nested data.
625+
*
626+
* @param \ArrayAccess|array $source
627+
*
628+
* credit: https://github.com/FranBar1966/FlatPHP
629+
*/
630+
private static function flattenArray(
631+
mixed $source,
632+
array &$destination,
633+
string $start = '',
634+
) {
635+
$opt = [
636+
'prefix' => '[',
637+
'suffix' => ']',
638+
'suffix-end' => true,
639+
'prefix-list' => '[',
640+
'suffix-list' => ']',
641+
'suffix-list-end' => true,
642+
];
643+
644+
if (!is_array($source)) {
645+
$source = (array) $source;
646+
}
647+
648+
/**
649+
* array_is_list only in PHP >= 8.1
650+
*
651+
* credit: https://www.php.net/manual/en/function.array-is-list.php#127044
652+
*/
653+
if (!function_exists('array_is_list')) {
654+
function array_is_list(array $array)
655+
{
656+
$i = -1;
657+
658+
foreach ($array as $k => $v) {
659+
++$i;
660+
if ($k !== $i) {
661+
return false;
662+
}
663+
}
664+
665+
return true;
666+
}
667+
}
668+
669+
if (array_is_list($source)) {
670+
$currentPrefix = $opt['prefix-list'];
671+
$currentSuffix = $opt['suffix-list'];
672+
$currentSuffixEnd = $opt['suffix-list-end'];
673+
} else {
674+
$currentPrefix = $opt['prefix'];
675+
$currentSuffix = $opt['suffix'];
676+
$currentSuffixEnd = $opt['suffix-end'];
677+
}
678+
679+
$currentName = $start;
680+
681+
foreach ($source as $key => $val) {
682+
$currentName .= $currentPrefix.$key;
683+
684+
if (is_array($val) && !empty($val)) {
685+
$currentName .= "{$currentSuffix}";
686+
self::flattenArray($val, $destination, $currentName);
687+
} else {
688+
if ($currentSuffixEnd) {
689+
$currentName .= $currentSuffix;
690+
}
691+
$destination[$currentName] = self::toString($val);
692+
}
693+
694+
$currentName = $start;
695+
}
696+
}
608697
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -677,13 +677,13 @@ use {{invokerPackage}}\ObjectSerializer;
677677
$paramFiles = is_array(${{paramName}}) ? ${{paramName}} : [${{paramName}}];
678678
foreach ($paramFiles as $paramFile) {
679679
$formParams['{{baseName}}'][] = \GuzzleHttp\Psr7\Utils::tryFopen(
680-
ObjectSerializer::toFormValue($paramFile),
680+
ObjectSerializer::toFormValue('{{baseName}}', $paramFile)['{{baseName}}'],
681681
'rb'
682682
);
683683
}
684684
{{/isFile}}
685685
{{^isFile}}
686-
$formParams['{{baseName}}'] = ObjectSerializer::toFormValue(${{paramName}});
686+
$formParams = array_merge($formParams, ObjectSerializer::toFormValue('{{baseName}}', ${{paramName}}));
687687
{{/isFile}}
688688
}
689689
{{/formParams}}

modules/openapi-generator/src/main/resources/php/libraries/psr-18/api.mustache

+2-2
Original file line numberDiff line numberDiff line change
@@ -593,13 +593,13 @@ use function sprintf;
593593
$paramFiles = is_array(${{paramName}}) ? ${{paramName}} : [${{paramName}}];
594594
foreach ($paramFiles as $paramFile) {
595595
$formParams['{{baseName}}'][] = \GuzzleHttp\Psr7\try_fopen(
596-
ObjectSerializer::toFormValue($paramFile),
596+
ObjectSerializer::toFormValue('{{baseName}}', $paramFile)['{{baseName}}'],
597597
'rb'
598598
);
599599
}
600600
{{/isFile}}
601601
{{^isFile}}
602-
$formParams['{{baseName}}'] = ObjectSerializer::toFormValue(${{paramName}});
602+
$formParams = array_merge($formParams, ObjectSerializer::toFormValue('{{baseName}}', ${{paramName}}));
603603
{{/isFile}}
604604
}
605605
{{/formParams}}

modules/openapi-generator/src/test/resources/3_0/php/petstore-with-fake-endpoints-models-for-testing.yaml

+79
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,37 @@ paths:
311311
description: file to upload
312312
type: string
313313
format: binary
314+
'/pet/{petId}/uploadImageFullFormData':
315+
post:
316+
tags:
317+
- pet
318+
summary: uploads an image attached to a Pet object as formdata
319+
description: ''
320+
operationId: uploadImageFullFormData
321+
parameters:
322+
- name: petId
323+
in: path
324+
description: ID of pet to update
325+
required: true
326+
schema:
327+
type: integer
328+
format: int64
329+
responses:
330+
'200':
331+
description: successful operation
332+
content:
333+
application/json:
334+
schema:
335+
$ref: '#/components/schemas/ApiResponse'
336+
security:
337+
- petstore_auth:
338+
- 'write:pets'
339+
- 'read:pets'
340+
requestBody:
341+
content:
342+
multipart/form-data:
343+
schema:
344+
$ref: '#/components/schemas/PetWithFile'
314345
/store/inventory:
315346
get:
316347
tags:
@@ -1566,6 +1597,54 @@ components:
15661597
- sold
15671598
xml:
15681599
name: Pet
1600+
PetWithFile:
1601+
type: object
1602+
required:
1603+
- name
1604+
- photoUrls
1605+
properties:
1606+
id:
1607+
type: integer
1608+
format: int64
1609+
x-is-unique: true
1610+
category:
1611+
$ref: '#/components/schemas/Category'
1612+
name:
1613+
type: string
1614+
example: doggie
1615+
photoUrls:
1616+
type: array
1617+
xml:
1618+
name: photoUrl
1619+
wrapped: true
1620+
items:
1621+
type: string
1622+
uniqueItems: true
1623+
tags:
1624+
type: array
1625+
xml:
1626+
name: tag
1627+
wrapped: true
1628+
items:
1629+
$ref: '#/components/schemas/Tag'
1630+
status:
1631+
type: string
1632+
description: pet status in the store
1633+
enum:
1634+
- available
1635+
- pending
1636+
- sold
1637+
file:
1638+
description: file to upload
1639+
type: string
1640+
format: binary
1641+
multiple_files:
1642+
type: array
1643+
items:
1644+
type: string
1645+
format: binary
1646+
xml:
1647+
name: PetWithFile
15691648
ApiResponse:
15701649
type: object
15711650
properties:

samples/client/echo_api/php-nextgen-streaming/src/Api/BodyApi.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,7 @@ public function testBodyMultipartFormdataArrayOfBinaryRequest(
10341034
$formParams['files'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
10351035
? $paramFile
10361036
: \GuzzleHttp\Psr7\Utils::tryFopen(
1037-
ObjectSerializer::toFormValue($paramFile),
1037+
ObjectSerializer::toFormValue('files', $paramFile)['files'],
10381038
'rb'
10391039
);
10401040
}
@@ -1357,7 +1357,7 @@ public function testBodyMultipartFormdataSingleBinaryRequest(
13571357
$formParams['my-file'][] = $paramFile instanceof \Psr\Http\Message\StreamInterface
13581358
? $paramFile
13591359
: \GuzzleHttp\Psr7\Utils::tryFopen(
1360-
ObjectSerializer::toFormValue($paramFile),
1360+
ObjectSerializer::toFormValue('my-file', $paramFile)['my-file'],
13611361
'rb'
13621362
);
13631363
}

0 commit comments

Comments
 (0)