Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PHP] - Add FormDataProcessor to handle nested ModelInterface data #20990

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

namespace {{invokerPackage}};

use ArrayAccess;
use DateTimeInterface;
use DateTime;
use GuzzleHttp\Psr7\Utils;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use GuzzleHttp\Promise\PromiseInterface;
use {{invokerPackage}}\ApiException;
use {{invokerPackage}}\Configuration;
use {{invokerPackage}}\HeaderSelector;
use {{invokerPackage}}\FormDataProcessor;
use {{invokerPackage}}\ObjectSerializer;

/**
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading