From 4e3db234dcfac7e30d4d13bd61cc9fdd5fd7e437 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:56:37 -0400 Subject: [PATCH 01/13] feat(php): handle multipart data in function executions --- src/SDK/Language/PHP.php | 153 +++++++++--------- templates/php/base/params.twig | 8 +- templates/php/base/requests/api.twig | 5 +- templates/php/docs/example.md.twig | 4 +- templates/php/src/Client.php.twig | 16 +- templates/php/src/InputFile.php.twig | 51 ------ templates/php/src/Payload.php.twig | 98 +++++++++++ templates/php/src/Services/Service.php.twig | 4 +- .../php/tests/Services/ServiceTest.php.twig | 4 +- 9 files changed, 206 insertions(+), 137 deletions(-) delete mode 100644 templates/php/src/InputFile.php.twig create mode 100644 templates/php/src/Payload.php.twig diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index 0cdae26da..9108d4ffc 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -137,110 +137,110 @@ public function getFiles(): array { return [ [ - 'scope' => 'default', - 'destination' => 'README.md', - 'template' => 'php/README.md.twig', + 'scope' => 'default', + 'destination' => 'README.md', + 'template' => 'php/README.md.twig', //'block' => 'default', ], [ - 'scope' => 'default', - 'destination' => 'CHANGELOG.md', - 'template' => 'php/CHANGELOG.md.twig', + 'scope' => 'default', + 'destination' => 'CHANGELOG.md', + 'template' => 'php/CHANGELOG.md.twig', ], [ - 'scope' => 'default', - 'destination' => 'LICENSE', - 'template' => 'php/LICENSE.twig', + 'scope' => 'default', + 'destination' => 'LICENSE', + 'template' => 'php/LICENSE.twig', ], [ - 'scope' => 'default', - 'destination' => 'composer.json', - 'template' => 'php/composer.json.twig', + 'scope' => 'default', + 'destination' => 'composer.json', + 'template' => 'php/composer.json.twig', ], [ - 'scope' => 'service', - 'destination' => 'docs/{{service.name | caseLower}}.md', - 'template' => 'php/docs/service.md.twig', + 'scope' => 'service', + 'destination' => 'docs/{{service.name | caseLower}}.md', + 'template' => 'php/docs/service.md.twig', ], [ - 'scope' => 'method', - 'destination' => 'docs/examples/{{service.name | caseLower}}/{{method.name | caseDash}}.md', - 'template' => 'php/docs/example.md.twig', + 'scope' => 'method', + 'destination' => 'docs/examples/{{service.name | caseLower}}/{{method.name | caseDash}}.md', + 'template' => 'php/docs/example.md.twig', ], [ - 'scope' => 'default', - 'destination' => 'src/{{ spec.title | caseUcfirst}}/Client.php', - 'template' => 'php/src/Client.php.twig', + 'scope' => 'default', + 'destination' => 'src/{{ spec.title | caseUcfirst}}/Client.php', + 'template' => 'php/src/Client.php.twig', ], [ - 'scope' => 'default', - 'destination' => 'src/{{ spec.title | caseUcfirst}}/Permission.php', - 'template' => 'php/src/Permission.php.twig', + 'scope' => 'default', + 'destination' => 'src/{{ spec.title | caseUcfirst}}/Permission.php', + 'template' => 'php/src/Permission.php.twig', ], [ - 'scope' => 'default', - 'destination' => 'tests/{{ spec.title | caseUcfirst}}/PermissionTest.php', - 'template' => 'php/tests/PermissionTest.php.twig', + 'scope' => 'default', + 'destination' => 'tests/{{ spec.title | caseUcfirst}}/PermissionTest.php', + 'template' => 'php/tests/PermissionTest.php.twig', ], [ - 'scope' => 'default', - 'destination' => 'src/{{ spec.title | caseUcfirst}}/Role.php', - 'template' => 'php/src/Role.php.twig', + 'scope' => 'default', + 'destination' => 'src/{{ spec.title | caseUcfirst}}/Role.php', + 'template' => 'php/src/Role.php.twig', ], [ - 'scope' => 'default', - 'destination' => 'tests/{{ spec.title | caseUcfirst}}/RoleTest.php', - 'template' => 'php/tests/RoleTest.php.twig', + 'scope' => 'default', + 'destination' => 'tests/{{ spec.title | caseUcfirst}}/RoleTest.php', + 'template' => 'php/tests/RoleTest.php.twig', ], [ - 'scope' => 'default', - 'destination' => 'src/{{ spec.title | caseUcfirst}}/ID.php', - 'template' => 'php/src/ID.php.twig', + 'scope' => 'default', + 'destination' => 'src/{{ spec.title | caseUcfirst}}/ID.php', + 'template' => 'php/src/ID.php.twig', ], [ - 'scope' => 'default', - 'destination' => 'tests/{{ spec.title | caseUcfirst}}/IDTest.php', - 'template' => 'php/tests/IDTest.php.twig', + 'scope' => 'default', + 'destination' => 'tests/{{ spec.title | caseUcfirst}}/IDTest.php', + 'template' => 'php/tests/IDTest.php.twig', ], [ - 'scope' => 'default', - 'destination' => 'src/{{ spec.title | caseUcfirst}}/Query.php', - 'template' => 'php/src/Query.php.twig', + 'scope' => 'default', + 'destination' => 'src/{{ spec.title | caseUcfirst}}/Query.php', + 'template' => 'php/src/Query.php.twig', ], [ - 'scope' => 'default', - 'destination' => 'tests/{{ spec.title | caseUcfirst}}/QueryTest.php', - 'template' => 'php/tests/QueryTest.php.twig', + 'scope' => 'default', + 'destination' => 'tests/{{ spec.title | caseUcfirst}}/QueryTest.php', + 'template' => 'php/tests/QueryTest.php.twig', ], [ - 'scope' => 'default', - 'destination' => 'src/{{ spec.title | caseUcfirst}}/InputFile.php', - 'template' => 'php/src/InputFile.php.twig', + 'scope' => 'default', + 'destination' => 'src/{{ spec.title | caseUcfirst}}/Payload.php', + 'template' => 'php/src/Payload.php.twig', ], [ - 'scope' => 'default', - 'destination' => 'src/{{ spec.title | caseUcfirst}}/{{ spec.title | caseUcfirst}}Exception.php', - 'template' => 'php/src/Exception.php.twig', + 'scope' => 'default', + 'destination' => 'src/{{ spec.title | caseUcfirst}}/{{ spec.title | caseUcfirst}}Exception.php', + 'template' => 'php/src/Exception.php.twig', ], [ - 'scope' => 'default', - 'destination' => '/src/{{ spec.title | caseUcfirst}}/Service.php', - 'template' => 'php/src/Service.php.twig', + 'scope' => 'default', + 'destination' => '/src/{{ spec.title | caseUcfirst}}/Service.php', + 'template' => 'php/src/Service.php.twig', ], [ - 'scope' => 'service', - 'destination' => '/src/{{ spec.title | caseUcfirst}}/Services/{{service.name | caseUcfirst}}.php', - 'template' => 'php/src/Services/Service.php.twig', + 'scope' => 'service', + 'destination' => '/src/{{ spec.title | caseUcfirst}}/Services/{{service.name | caseUcfirst}}.php', + 'template' => 'php/src/Services/Service.php.twig', ], [ - 'scope' => 'service', - 'destination' => '/tests/{{ spec.title | caseUcfirst}}/Services/{{service.name | caseUcfirst}}Test.php', - 'template' => 'php/tests/Services/ServiceTest.php.twig', + 'scope' => 'service', + 'destination' => '/tests/{{ spec.title | caseUcfirst}}/Services/{{service.name | caseUcfirst}}Test.php', + 'template' => 'php/tests/Services/ServiceTest.php.twig', ], [ - 'scope' => 'enum', - 'destination' => '/src/{{ spec.title | caseUcfirst}}/Enums/{{ enum.name | caseUcfirst }}.php', - 'template' => 'php/src/Enums/Enum.php.twig', + 'scope' => 'enum', + 'destination' => '/src/{{ spec.title | caseUcfirst}}/Enums/{{ enum.name | caseUcfirst }}.php', + 'template' => 'php/src/Enums/Enum.php.twig', ], ]; } @@ -258,6 +258,11 @@ public function getTypeName(array $parameter, array $spec = []): string if (!empty($parameter['enumValues'])) { return \ucfirst($parameter['name']); } + + if ($parameter['name'] === 'body' && strpos($parameter['description'], 'body of execution') !== false) { + return 'Payload'; + } + return match ($parameter['type']) { self::TYPE_STRING => 'string', self::TYPE_BOOLEAN => 'bool', @@ -265,7 +270,7 @@ public function getTypeName(array $parameter, array $spec = []): string self::TYPE_INTEGER => 'int', self::TYPE_ARRAY, self::TYPE_OBJECT => 'array', - self::TYPE_FILE => 'InputFile', + self::TYPE_FILE => 'Payload', default => $parameter['type'], }; } @@ -276,9 +281,9 @@ public function getTypeName(array $parameter, array $spec = []): string */ public function getParamDefault(array $param): string { - $type = $param['type'] ?? ''; - $default = $param['default'] ?? ''; - $required = $param['required'] ?? ''; + $type = $param['type'] ?? ''; + $default = $param['default'] ?? ''; + $required = $param['required'] ?? ''; if ($required) { return ''; @@ -329,8 +334,8 @@ public function getParamDefault(array $param): string */ public function getParamExample(array $param): string { - $type = $param['type'] ?? ''; - $example = $param['example'] ?? ''; + $type = $param['type'] ?? ''; + $example = $param['example'] ?? ''; $output = ''; @@ -349,7 +354,7 @@ public function getParamExample(array $param): string $output .= '[]'; break; case self::TYPE_FILE: - $output .= "InputFile::withPath('file.png')"; + $output .= "Payload::fromPath('file.png')"; break; } } else { @@ -366,10 +371,14 @@ public function getParamExample(array $param): string $output .= ($example) ? 'true' : 'false'; break; case self::TYPE_STRING: - $output .= "'{$example}'"; + if ($param['name'] === 'body' && strpos($param['description'], 'body of execution') !== false) { + $output .= "Payload::fromJson([])"; + } else { + $output .= "'{$example}'"; + } break; case self::TYPE_FILE: - $output .= "InputFile::withPath('file.png')"; + $output .= "Payload::fromPath('file.png')"; break; } } diff --git a/templates/php/base/params.twig b/templates/php/base/params.twig index 67a6fee93..a104bcd4a 100644 --- a/templates/php/base/params.twig +++ b/templates/php/base/params.twig @@ -14,7 +14,11 @@ {% endfor %} {% for parameter in method.parameters.body %} if (!is_null(${{ parameter.name | caseCamel | escapeKeyword }})) { - $apiParams['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; + {%~ if method.name | caseLower == "createexecution" and parameter.name == 'body' %} + $apiParams['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}->toBinary(); + {%~ else %} + $apiParams['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; + {%~ endif %} } {% endfor %} {% for parameter in method.parameters.formData %} @@ -22,4 +26,4 @@ $apiParams['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; } {% endfor %} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/templates/php/base/requests/api.twig b/templates/php/base/requests/api.twig index acb6aadd5..4bf8e3a44 100644 --- a/templates/php/base/requests/api.twig +++ b/templates/php/base/requests/api.twig @@ -7,8 +7,11 @@ {%~ endfor %} {%~ for key, header in method.headers %} '{{ key }}' => '{{ header }}', + {%~ if method.name | lower == "createexecution" %} + 'accept' => 'multipart/form-data', + {%~ endif %} {%~ endfor %} ], $apiParams{% if method.type == 'webAuth' -%}, 'location'{% endif %} - ); \ No newline at end of file + ); diff --git a/templates/php/docs/example.md.twig b/templates/php/docs/example.md.twig index ded2f259d..686c0ea62 100644 --- a/templates/php/docs/example.md.twig +++ b/templates/php/docs/example.md.twig @@ -1,8 +1,8 @@ param.type == 'file') | length > 0 %} -use {{ spec.title | caseUcfirst }}\InputFile; +{% if method.parameters.all | filter((param) => param.type == 'file') | length > 0 or method.name | caseLower == 'createexecution' %} +use {{ spec.title | caseUcfirst }}\Payload; {% endif %} use {{ spec.title | caseUcfirst }}\Services\{{ service.name | caseUcfirst }}; {% set added = [] %} diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index 540835c5e..c80994145 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -51,7 +51,7 @@ class Client { {% for key,header in spec.global.defaultHeaders %} $this->headers['{{key}}'] = '{{header}}'; -{% endfor %} +{% endfor %} } {% for header in spec.global.headers %} @@ -104,7 +104,7 @@ class Client public function addHeader($key, $value) { $this->headers[strtolower($key)] = $value; - + return $this; } @@ -183,17 +183,23 @@ class Client echo 'Warning: ' . $warning . PHP_EOL; } } - + switch(substr($contentType, 0, strpos($contentType, ';'))) { case 'application/json': $responseBody = json_decode($responseBody, true); break; } - + if ($contentType === 'multipart/form-data') { + $matches = []; + preg_match('/(?[-]+[\w]+)--/m', $responseBody, $matches); + if (isset($matches['boundary'])) { + $responseBody = Payload::handleFormData($matches['boundary'], $responseBody); + } + } if (curl_errno($ch)) { throw new {{spec.title | caseUcfirst}}Exception(curl_error($ch), $responseStatus, $responseBody); } - + curl_close($ch); if($responseStatus >= 400) { diff --git a/templates/php/src/InputFile.php.twig b/templates/php/src/InputFile.php.twig deleted file mode 100644 index a7822196b..000000000 --- a/templates/php/src/InputFile.php.twig +++ /dev/null @@ -1,51 +0,0 @@ -data; - } - - public function getPath(): ?string - { - return $this->path; - } - - public function getMimeType(): ?string - { - return $this->mimeType; - } - - public function getFilename(): ?string - { - return $this->filename; - } - - public static function withPath(string $path, ?string $mimeType = null, ?string $filename = null) - { - $instance = new InputFile(); - $instance->path = $path; - $instance->data = null; - $instance->mimeType = $mimeType; - $instance->filename = $filename; - return $instance; - } - - public static function withData(string $data, ?string $mimeType = null, ?string $filename = null) - { - $instance = new InputFile(); - $instance->path = null; - $instance->data = $data; - $instance->mimeType = $mimeType; - $instance->filename = $filename; - return $instance; - } -} \ No newline at end of file diff --git a/templates/php/src/Payload.php.twig b/templates/php/src/Payload.php.twig new file mode 100644 index 000000000..97ac75fa4 --- /dev/null +++ b/templates/php/src/Payload.php.twig @@ -0,0 +1,98 @@ +data; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getMimeType(): ?string + { + return $this->mimeType; + } + + public function getFilename(): ?string + { + return $this->filename; + } + + public static function fromPath(string $path, ?string $mimeType = null, ?string $filename = null) + { + $instance = new Payload(); + $instance->path = $path; + $instance->data = null; + $instance->mimeType = $mimeType; + $instance->filename = $filename; + return $instance; + } + + public static function fromData(string $data, ?string $mimeType = null, ?string $filename = null) + { + $instance = new Payload(); + $instance->path = null; + $instance->data = $data; + $instance->mimeType = $mimeType; + $instance->filename = $filename; + return $instance; + } + + public static function fromJson(array $data) { + $instance = new Payload(); + $instance->path = null; + $instance->data = json_encode($data); + return $instance; + } + + public static function fromString(string $data) { + $instance = new Payload(); + $instance->path = null; + $instance->data = $data; + return $instance; + } + + public function toBinary(): string + { + return $this->data; + } + + public function toJson(): mixed + { + return json_decode($this->data, true); + } + + public function toString(): string + { + return $this->data; + } + + public static function handleFormData(string $boundary, mixed $responseBody) + { + $parts = explode($boundary, $responseBody); + $data = []; + foreach ($parts as $part) { + $lines = array_values(array_filter(explode("\r\n", $part))); + $matches = []; + $matched = preg_match('/name="?(?\w+)/s', $part, $matches); + if ($matched) { + $data[$matches['name']] = $lines[1] ?? '';; + } + } + + $data['responseBody'] = self::fromString($data['responseBody'] ?? ''); + + return $data; + } +} diff --git a/templates/php/src/Services/Service.php.twig b/templates/php/src/Services/Service.php.twig index 8d0b54201..9d1c1e183 100644 --- a/templates/php/src/Services/Service.php.twig +++ b/templates/php/src/Services/Service.php.twig @@ -5,7 +5,7 @@ namespace {{ spec.title | caseUcfirst }}\Services; use {{ spec.title | caseUcfirst }}\{{spec.title | caseUcfirst}}Exception; use {{ spec.title | caseUcfirst }}\Client; use {{ spec.title | caseUcfirst }}\Service; -use {{ spec.title | caseUcfirst }}\InputFile; +use {{ spec.title | caseUcfirst }}\Payload; {% set added = [] %} {% for method in service.methods %} {% for parameter in method.parameters.all %} @@ -50,7 +50,7 @@ class {{ service.name | caseUcfirst }} extends Service $apiPath = str_replace([{% for parameter in method.parameters.path %}'{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}'{% if not loop.last %}, {% endif %}{% endfor %}], [{% for parameter in method.parameters.path %}${{ parameter.name | caseCamel | escapeKeyword }}{% if not loop.last %}, {% endif %}{% endfor %}], '{{ method.path }}'); {{~ include('php/base/params.twig') -}} - {%~ if 'multipart/form-data' in method.consumes %} + {%~ if 'multipart/form-data' in method.consumes and method.name | lower != "createexecution" %} {{~ include('php/base/requests/file.twig') }} {%~ else %} {{~ include('php/base/requests/api.twig') }} diff --git a/templates/php/tests/Services/ServiceTest.php.twig b/templates/php/tests/Services/ServiceTest.php.twig index 6e31e9bf3..1d3e40465 100644 --- a/templates/php/tests/Services/ServiceTest.php.twig +++ b/templates/php/tests/Services/ServiceTest.php.twig @@ -3,7 +3,7 @@ namespace Appwrite\Services; use Appwrite\Client; -use Appwrite\InputFile; +use Appwrite\Payload; use Mockery; use PHPUnit\Framework\TestCase; @@ -34,7 +34,7 @@ final class {{service.name | caseUcfirst}}Test extends TestCase { ->andReturn($data); $response = $this->{{service.name | caseCamel}}->{{method.name | caseCamel}}({%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} - {% if parameter.type == 'object' %}array(){% elseif parameter.type == 'array' %}array(){% elseif parameter.type == 'file' %}InputFile::withData('', "image/png"){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'string' %}"{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}"{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%}{% if not loop.last %},{% endif %}{%~ endfor ~%} + {% if parameter.type == 'object' %}array(){% elseif parameter.type == 'array' %}array(){% elseif parameter.type == 'file' %}Payload::fromData('', "image/png"){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'string' %}"{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}"{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%}{% if not loop.last %},{% endif %}{%~ endfor ~%} ); $this->assertSame($data, $response); From 723e2b963fcf0b5d0fc49b62ae9094a651f6fba6 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:02:02 -0400 Subject: [PATCH 02/13] feat(php): converting parts to type --- templates/php/src/Payload.php.twig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/php/src/Payload.php.twig b/templates/php/src/Payload.php.twig index 97ac75fa4..ca8f60914 100644 --- a/templates/php/src/Payload.php.twig +++ b/templates/php/src/Payload.php.twig @@ -91,6 +91,8 @@ class Payload { } } + $data['responseStatusCode'] = (int) ($data['responseStatusCode'] ?? ''); + $data['duration'] = ((float) $data['duration'] ?? ''); $data['responseBody'] = self::fromString($data['responseBody'] ?? ''); return $data; From 081fac1cb947e46d9844da4b29db29b12c42ec4c Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:18:16 -0400 Subject: [PATCH 03/13] fix(php): adapting tests to the new Payload class --- tests/languages/php/test.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/languages/php/test.php b/tests/languages/php/test.php index f182257b5..bdae763ae 100644 --- a/tests/languages/php/test.php +++ b/tests/languages/php/test.php @@ -2,7 +2,7 @@ include __DIR__ . '/../../sdks/php/src/Appwrite/Client.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Service.php'; -include __DIR__ . '/../../sdks/php/src/Appwrite/InputFile.php'; +include __DIR__ . '/../../sdks/php/src/Appwrite/Payload.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Query.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Permission.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Role.php'; @@ -15,7 +15,7 @@ use Appwrite\AppwriteException; use Appwrite\Client; -use Appwrite\InputFile; +use Appwrite\Payload; use Appwrite\Query; use Appwrite\Permission; use Appwrite\Role; @@ -73,17 +73,17 @@ echo "{$response['result']}\n"; $data = file_get_contents(__DIR__ . '/../../resources/file.png'); -$response = $general->upload('string', 123, ['string in array'], InputFile::withData($data, 'image/png', 'file.png')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromData($data, 'image/png', 'file.png')); echo "{$response['result']}\n"; $data = file_get_contents(__DIR__ . '/../../resources/large_file.mp4'); -$response = $general->upload('string', 123, ['string in array'], InputFile::withData($data, 'video/mp4', 'large_file.mp4')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromData($data, 'video/mp4', 'large_file.mp4')); echo "{$response['result']}\n"; -$response = $general->upload('string', 123, ['string in array'], InputFile::withPath(__DIR__ .'/../../resources/file.png')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromPath(__DIR__ .'/../../resources/file.png')); echo "{$response['result']}\n"; -$response = $general->upload('string', 123, ['string in array'], InputFile::withPath(__DIR__ .'/../../resources/large_file.mp4')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromPath(__DIR__ .'/../../resources/large_file.mp4')); echo "{$response['result']}\n"; $response = $general->enum(MockType::FIRST()); From d3e6c0b955e65040baa3c15edd87c764be3e2165 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:00:04 -0400 Subject: [PATCH 04/13] fix(php): fixing multipart --- templates/php/src/Payload.php.twig | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/templates/php/src/Payload.php.twig b/templates/php/src/Payload.php.twig index ca8f60914..38c631da6 100644 --- a/templates/php/src/Payload.php.twig +++ b/templates/php/src/Payload.php.twig @@ -87,7 +87,18 @@ class Payload { $matches = []; $matched = preg_match('/name="?(?\w+)/s', $part, $matches); if ($matched) { - $data[$matches['name']] = $lines[1] ?? '';; + array_shift($lines); + if(isset($lines[0]) && $lines[0] === 'Content-Type: application/json'){ + array_shift($lines); + $headers = json_decode(implode($lines), true); + $headers = array_combine( + array_map(fn($header)=> $header['name'], $headers), + array_map(fn($header)=> $header['value'], $headers) + ); + $data[$matches['name']] = $headers; + continue; + } + $data[$matches['name']] = implode("",$lines) ?? '';; } } From eeee746f33a8d7b1aa1e5d5c482355b3e39de39d Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:42:18 -0400 Subject: [PATCH 05/13] feat(all): adding multipart test --- mock-server/Dockerfile | 1 + mock-server/app/http.php | 55 ++++++--- mock-server/composer.json | 3 +- mock-server/composer.lock | 107 +++++++++++----- mock-server/resources/file.png | Bin 0 -> 38756 bytes mock-server/src/Utopia/BodyMultipart.php | 151 +++++++++++++++++++++++ mock-server/src/Utopia/Response.php | 28 ++++- tests/Base.php | 5 + tests/resources/spec.json | 89 ++++++++++++- 9 files changed, 379 insertions(+), 60 deletions(-) create mode 100644 mock-server/resources/file.png create mode 100644 mock-server/src/Utopia/BodyMultipart.php diff --git a/mock-server/Dockerfile b/mock-server/Dockerfile index a1c3ed8aa..f048d39a4 100644 --- a/mock-server/Dockerfile +++ b/mock-server/Dockerfile @@ -30,6 +30,7 @@ COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor # Add Source Code COPY ./src /usr/src/code/src COPY ./app /usr/src/code/app +COPY ./resources /usr/src/code/resources EXPOSE 80 diff --git a/mock-server/app/http.php b/mock-server/app/http.php index 467cb48b8..3af0eeab6 100644 --- a/mock-server/app/http.php +++ b/mock-server/app/http.php @@ -63,8 +63,8 @@ ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->inject('response') - ->action(function (UtopiaSwooleResponse $response) { - $response->json([ 'version' => '1.0.0' ]); + ->action(function (Response $response) { + $response->json(['version' => '1.0.0']); }); // Mock Routes @@ -263,7 +263,7 @@ ->label('sdk.mock', true) ->inject('request') ->inject('response') - ->action(function (Request $request, UtopiaSwooleResponse $response) { + ->action(function (Request $request, Response $response) { $res = [ 'x-sdk-name' => $request->getHeader('x-sdk-name'), 'x-sdk-platform' => $request->getHeader('x-sdk-platform'), @@ -291,8 +291,7 @@ ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.mock', true) ->inject('response') - ->action(function (UtopiaSwooleResponse $response) { - + ->action(function (Response $response) { $response ->setContentType('text/plain') ->addHeader('Content-Disposition', 'attachment; filename="test.txt"') @@ -317,12 +316,11 @@ ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Integer(true), 'Sample numeric param') ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') - ->param('file', [], new File(), 'Sample file param', skipValidation: true) + ->param('payload', [], new File(), 'Sample file param', skipValidation: true) ->inject('request') ->inject('response') - ->action(function (string $x, int $y, array $z, mixed $file, Request $request, UtopiaSwooleResponse $response) { - - $file = $request->getFiles('file'); + ->action(function (string $x, int $y, array $z, mixed $file, Request $request, Response $response) { + $file = $request->getFiles('payload'); $contentRange = $request->getHeader('content-range'); @@ -366,7 +364,7 @@ if ($end !== $size - 1) { $response->json([ '$id' => ID::custom('newfileid'), - 'chunksTotal' => (int) ceil($size / ($end + 1 - $start)), + 'chunksTotal' => (int)ceil($size / ($end + 1 - $start)), 'chunksUploaded' => ceil($start / $chunkSize) + 1 ]); } @@ -389,6 +387,30 @@ } }); +App::get('/v1/mock/tests/general/multipart') + ->desc('Multipart') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'multipart') + ->label('sdk.description', 'Mock a multipart request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_MULTIPART) + ->label('sdk.response.model', Response::MODEL_MULTIPART) + ->label('sdk.mock', true) + ->inject('response') + ->action(function (Response $response) { + $file = \fread(\fopen(\getcwd() . '/resources/file.png', 'r'), \filesize(\getcwd() . '/resources/file.png')); + + $response->multipart([ + 'x' => 'abc', + 'y' => 123, + 'responseBody' => $file, + ]); + }); + + App::get('/v1/mock/tests/general/redirect') ->desc('Redirect') ->groups(['mock']) @@ -548,7 +570,6 @@ ->label('sdk.mock', true) ->inject('response') ->action(function (UtopiaSwooleResponse $response) { - $response ->setStatusCode(502) ->text('This is a text error'); @@ -645,7 +666,6 @@ ->label('docs', false) ->inject('response') ->action(function (UtopiaSwooleResponse $response) { - $response->json([ 'result' => 'success', ]); @@ -658,7 +678,6 @@ ->label('docs', false) ->inject('response') ->action(function (UtopiaSwooleResponse $response) { - $response ->setStatusCode(Response::STATUS_CODE_BAD_REQUEST) ->json([ @@ -672,11 +691,10 @@ ->inject('response') ->inject('request') ->action(function (App $utopia, UtopiaSwooleResponse $response, Request $request) { - $result = []; - $route = $utopia->getRoute(); - $path = APP_STORAGE_CACHE . '/tests.json'; - $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; + $route = $utopia->getRoute(); + $path = APP_STORAGE_CACHE . '/tests.json'; + $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; if (!\is_array($tests)) { throw new Exception(Exception::GENERAL_MOCK, 'Failed to read results', 500); @@ -779,8 +797,7 @@ function () use ($http) { $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { $request = new Request($swooleRequest); - $response = new UtopiaSwooleResponse($swooleResponse); - + $response = new Response($swooleResponse); $app = new App('UTC'); $app->run($request, $response); diff --git a/mock-server/composer.json b/mock-server/composer.json index 66ee3931c..9b01faaeb 100644 --- a/mock-server/composer.json +++ b/mock-server/composer.json @@ -10,7 +10,8 @@ "utopia-php/framework": "0.33.*", "utopia-php/database": "0.48.*", "utopia-php/cli": "0.16.*", - "utopia-php/swoole": "0.8.*" + "utopia-php/swoole": "0.8.*", + "utopia-php/fetch": "0.2.*" }, "require-dev": { "swoole/ide-helper": "5.1.2" diff --git a/mock-server/composer.lock b/mock-server/composer.lock index 9acf97cf3..2e308d819 100644 --- a/mock-server/composer.lock +++ b/mock-server/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e8e3df78a113bec48bb61da0227ea50f", + "content-hash": "b2b8c7f2f6927706fbb3f8a65a0e3752", "packages": [ { "name": "jean85/pretty-package-versions", - "version": "2.0.5", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", "shasum": "" }, "require": { @@ -25,9 +25,9 @@ "php": "^7.1|^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.17", + "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^0.12.66", + "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^7.5|^8.5|^9.4", "vimeo/psalm": "^4.3" }, @@ -61,9 +61,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" }, - "time": "2021-10-08T21:21:46+00:00" + "time": "2024-03-08T09:58:59+00:00" }, { "name": "mongodb/mongodb", @@ -136,16 +136,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -196,7 +196,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -212,20 +212,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "utopia-php/cache", - "version": "0.9.0", + "version": "0.9.1", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "4fc7b4789b5f0ce74835c1ecfec4f3afe6f0e34e" + "reference": "552b4c554bb14d0c529631ce304cdf4a2b9d06a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/4fc7b4789b5f0ce74835c1ecfec4f3afe6f0e34e", - "reference": "4fc7b4789b5f0ce74835c1ecfec4f3afe6f0e34e", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/552b4c554bb14d0c529631ce304cdf4a2b9d06a6", + "reference": "552b4c554bb14d0c529631ce304cdf4a2b9d06a6", "shasum": "" }, "require": { @@ -260,9 +260,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.9.0" + "source": "https://github.com/utopia-php/cache/tree/0.9.1" }, - "time": "2024-01-07T18:11:23+00:00" + "time": "2024-03-19T17:07:20+00:00" }, { "name": "utopia-php/cli", @@ -315,16 +315,16 @@ }, { "name": "utopia-php/database", - "version": "0.48.2", + "version": "0.48.4", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "0a231a2874fdbc0cf2ae2170b3f132fdee0ddfd4" + "reference": "02f20bd901b8fab26d7dc2c58f7da1d6a08d21c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/0a231a2874fdbc0cf2ae2170b3f132fdee0ddfd4", - "reference": "0a231a2874fdbc0cf2ae2170b3f132fdee0ddfd4", + "url": "https://api.github.com/repos/utopia-php/database/zipball/02f20bd901b8fab26d7dc2c58f7da1d6a08d21c0", + "reference": "02f20bd901b8fab26d7dc2c58f7da1d6a08d21c0", "shasum": "" }, "require": { @@ -332,7 +332,7 @@ "ext-pdo": "*", "php": ">=8.0", "utopia-php/cache": "0.9.*", - "utopia-php/framework": "0.*.*", + "utopia-php/framework": "0.33.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { @@ -365,22 +365,61 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.48.2" + "source": "https://github.com/utopia-php/database/tree/0.48.4" }, - "time": "2024-02-02T14:10:14+00:00" + "time": "2024-02-23T03:22:55+00:00" + }, + { + "name": "utopia-php/fetch", + "version": "0.2.1", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/fetch.git", + "reference": "1423c0ee3eef944d816ca6e31706895b585aea82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/fetch/zipball/1423c0ee3eef944d816ca6e31706895b585aea82", + "reference": "1423c0ee3eef944d816ca6e31706895b585aea82", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "^1.5.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Fetch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple library that provides an interface for making HTTP Requests.", + "support": { + "issues": "https://github.com/utopia-php/fetch/issues", + "source": "https://github.com/utopia-php/fetch/tree/0.2.1" + }, + "time": "2024-03-18T11:50:59+00:00" }, { "name": "utopia-php/framework", - "version": "0.33.2", + "version": "0.33.8", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "b1423ca3e3b61c6c4c2e619d2cb80672809a19f3" + "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/b1423ca3e3b61c6c4c2e619d2cb80672809a19f3", - "reference": "b1423ca3e3b61c6c4c2e619d2cb80672809a19f3", + "url": "https://api.github.com/repos/utopia-php/http/zipball/a7f577540a25cb90896fef2b64767bf8d700f3c5", + "reference": "a7f577540a25cb90896fef2b64767bf8d700f3c5", "shasum": "" }, "require": { @@ -410,9 +449,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.2" + "source": "https://github.com/utopia-php/http/tree/0.33.8" }, - "time": "2024-01-31T10:35:59+00:00" + "time": "2024-08-15T14:10:09+00:00" }, { "name": "utopia-php/mongo", diff --git a/mock-server/resources/file.png b/mock-server/resources/file.png new file mode 100644 index 0000000000000000000000000000000000000000..688533b76407e1ac6c3495e558e9b8f8cb1a0fde GIT binary patch literal 38756 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 RE#k*-e%bm`mC4_a{s-kj{pSDx literal 0 HcmV?d00001 diff --git a/mock-server/src/Utopia/BodyMultipart.php b/mock-server/src/Utopia/BodyMultipart.php new file mode 100644 index 000000000..0acaa1378 --- /dev/null +++ b/mock-server/src/Utopia/BodyMultipart.php @@ -0,0 +1,151 @@ + $parts + */ + private array $parts = []; + private string $boundary = ""; + + public function __construct(string $boundary = null) + { + if (is_null($boundary)) { + $this->boundary = self::generateBoundary(); + } else { + $this->boundary = $boundary; + } + } + + public static function generateBoundary(): string + { + return '-----------------------------' . \uniqid(); + } + + public function load(string $body): self + { + $eol = "\r\n"; + + $sections = \explode('--' . $this->boundary, $body); + + foreach ($sections as $section) { + if (empty($section)) { + continue; + } + + if (strpos($section, $eol) === 0) { + $section = substr($section, \strlen($eol)); + } + + if (substr($section, -2) === $eol) { + $section = substr($section, 0, -1 * \strlen($eol)); + } + + if ($section == '--') { + continue; + } + + $partChunks = \explode($eol . $eol, $section, 2); + + if (\count($partChunks) < 2) { + continue; // Broken part + } + + [ $partHeaders, $partBody ] = $partChunks; + $partHeaders = \explode($eol, $partHeaders); + + $partName = ""; + foreach ($partHeaders as $partHeader) { + if (!empty($partName)) { + break; + } + + $partHeaderArray = \explode(':', $partHeader, 2); + + $partHeaderName = \strtolower($partHeaderArray[0] ?? ''); + $partHeaderValue = $partHeaderArray[1] ?? ''; + if ($partHeaderName == "content-disposition") { + $dispositionChunks = \explode("; ", $partHeaderValue); + foreach ($dispositionChunks as $dispositionChunk) { + $dispositionChunkValues = \explode("=", $dispositionChunk, 2); + if (\count($dispositionChunkValues) >= 2) { + if ($dispositionChunkValues[0] === "name") { + $partName = \trim($dispositionChunkValues[1], "\""); + break; + } + } + } + } + } + + if (!empty($partName)) { + $this->parts[$partName] = $partBody; + } + } + return $this; + } + + /** + * @return array + */ + public function getParts(): array + { + return $this->parts ?? []; + } + + public function getPart(string $key, mixed $default = ''): mixed + { + return $this->parts[$key] ?? $default; + } + + public function setPart(string $key, mixed $value): self + { + $this->parts[$key] = $value; + return $this; + } + + public function getBoundary(): string + { + return $this->boundary; + } + + public function setBoundary(string $boundary): self + { + $this->boundary = $boundary; + return $this; + } + + public function exportHeader(): string + { + return 'multipart/form-data; boundary=' . $this->boundary; + } + + public function exportBody(): string + { + $eol = "\r\n"; + $query = '--' . $this->boundary; + + foreach ($this->parts as $key => $value) { + $query .= $eol . 'Content-Disposition: form-data; name="' . $key . '"'; + + if (\is_array($value)) { + $query .= $eol . 'Content-Type: application/json'; + $value = \json_encode($value); + } + + $query .= $eol . $eol; + if ($value === false) { + $query .= 0 . $eol; + } else { + $query .= $value . $eol; + } + $query .= '--' . $this->boundary; + } + + $query .= "--" . $eol; + + return $query; + } +} diff --git a/mock-server/src/Utopia/Response.php b/mock-server/src/Utopia/Response.php index 756049364..dec8c6ecc 100644 --- a/mock-server/src/Utopia/Response.php +++ b/mock-server/src/Utopia/Response.php @@ -2,14 +2,17 @@ namespace Utopia\MockServer\Utopia; -use Utopia\Swoole\Response as SwooleResponse; +use Utopia\MockServer\Utopia\BodyMultipart; +use Swoole\Http\Response as SwooleResponse; +use Utopia\CLI\Console; use Utopia\Database\Document; +use Utopia\Swoole\Response as UtopiaResponse; /** * @method int getStatusCode() * @method Response setStatusCode(int $code = 200) */ -class Response extends SwooleResponse +class Response extends UtopiaResponse { // General public const MODEL_NONE = 'none'; @@ -21,7 +24,7 @@ class Response extends SwooleResponse public const MODEL_METRIC_LIST = 'metricList'; public const MODEL_ERROR_DEV = 'errorDev'; public const MODEL_BASE_LIST = 'baseList'; - + public const MODEL_MULTIPART = 'multipart'; // Mock public const MODEL_MOCK = 'mock'; @@ -39,6 +42,7 @@ class Response extends SwooleResponse */ public function __construct(SwooleResponse $response) { + parent::__construct($response); } /** @@ -46,7 +50,7 @@ public function __construct(SwooleResponse $response) */ public const CONTENT_TYPE_YAML = 'application/x-yaml'; public const CONTENT_TYPE_NULL = 'null'; - + public const CONTENT_TYPE_MULTIPART = 'multipart/form-data'; /** * List of defined output objects */ @@ -91,6 +95,18 @@ public function getModels(): array return $this->models; } + public function multipart(array $data): void + { + $multipart = new BodyMultipart(); + foreach ($data as $key => $value) { + $multipart->setPart($key, $value); + } + + $this + ->setContentType($multipart->exportHeader()) + ->send($multipart->exportBody()); + } + /** * Validate response objects and outputs * the response according to given format type @@ -118,6 +134,10 @@ public function dynamic(Document $document, string $model): void case self::CONTENT_TYPE_NULL: break; + case self::CONTENT_TYPE_MULTIPART: + $this->multipart(!empty($output) ? $output : new \stdClass()); + break; + default: if ($model === self::MODEL_NONE) { $this->noContent(); diff --git a/tests/Base.php b/tests/Base.php index 201b63a39..3427ae9cd 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -74,6 +74,11 @@ abstract class Base extends TestCase 'WS:/v1/realtime:passed', ]; + protected const MULTIPART_RESPONSES = [ + 'abc', + 'd80e7e6999a3eb2ae0d631a96fe135a4' # + ]; + protected const QUERY_HELPER_RESPONSES = [ '{"method":"equal","attribute":"released","values":[true]}', '{"method":"equal","attribute":"title","values":["Spiderman","Dr. Strange"]}', diff --git a/tests/resources/spec.json b/tests/resources/spec.json index d3ddf0676..d2694225d 100644 --- a/tests/resources/spec.json +++ b/tests/resources/spec.json @@ -1510,6 +1510,58 @@ ] } }, + "\/mock\/tests\/general\/multipart": { + "get": { + "summary": "Multipart", + "operationId": "generalMultipart", + "consumes": [ + "application\/json" + ], + "produces": [ + "multipart\/form-data" + ], + "tags": [ + "general" + ], + "description": "", + "responses": { + "301": { + "description": "No content" + } + }, + "x-appwrite": { + "method": "multipart", + "weight": 278, + "cookies": false, + "type": "", + "demo": "general\/multipart.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterMock a multipart request.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "public", + "platforms": [ + "client", + "server", + "server" + ], + "packaging": false, + "offline-model": "", + "offline-key": "", + "offline-response-key": "$id", + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [], + "JWT": [] + } + ] + } + }, "\/mock\/tests\/general\/redirect": { "get": { "summary": "Redirect", @@ -1754,7 +1806,7 @@ "in": "formData" }, { - "name": "file", + "name": "payload", "description": "Sample file param", "required": true, "type": "file", @@ -1979,6 +2031,39 @@ "version" ] }, + "multipart": { + "description": "Multipart", + "type": "object", + "properties": { + "x": { + "type": "string", + "description": "Sample string param", + "default": null, + "x-example": "[]" + }, + "y": { + "type": "integer", + "description": "Sample numeric param", + "default": null, + "x-example": null + }, + "z": { + "type": "array", + "description": "Sample array param", + "default": null, + "x-example": null, + "items": { + "type": "string" + } + }, + "body": { + "type": "file", + "description": "Sample file param", + "default": null, + "x-example": null + } + } + }, "mock": { "description": "Mock", "type": "object", @@ -1998,4 +2083,4 @@ "description": "Full API docs, specs and tutorials", "url": "https:\/\/appwrite.io\/docs" } -} \ No newline at end of file +} From 3c6db890c5067a4ae2b05c4069a5f766a8074e06 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:42:28 -0400 Subject: [PATCH 06/13] feat(php): adding multipart test --- templates/php/base/requests/file.twig | 6 +++--- tests/PHP74Test.php | 1 + tests/PHP80Test.php | 1 + tests/languages/php/test.php | 21 +++++++++++++-------- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/templates/php/base/requests/file.twig b/templates/php/base/requests/file.twig index e80c793da..156c83cc7 100644 --- a/templates/php/base/requests/file.twig +++ b/templates/php/base/requests/file.twig @@ -65,7 +65,7 @@ fseek($handle, $start); $chunk = @fread($handle, Client::CHUNK_SIZE); } else { - $chunk = substr($file->getData(), $start, Client::CHUNK_SIZE); + $chunk = substr($payload->getData(), $start, Client::CHUNK_SIZE); } $apiParams['{{ parameter.name }}'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($chunk), $mimeType, $postedName); $apiHeaders['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size - 1) . '/' . $size; @@ -84,7 +84,7 @@ 'progress' => min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE)), $size) / $size * 100, 'sizeUploaded' => min($counter * Client::CHUNK_SIZE), 'chunksTotal' => $response['chunksTotal'], - 'chunksUploaded' => $response['chunksUploaded'], + 'chunksUploaded' => $response['chunksUploaded'], ]); } } @@ -93,4 +93,4 @@ } return $response; {% endif %} -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/tests/PHP74Test.php b/tests/PHP74Test.php index ac53020b6..406e974d2 100644 --- a/tests/PHP74Test.php +++ b/tests/PHP74Test.php @@ -23,6 +23,7 @@ class PHP74Test extends Base ...Base::ENUM_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, + ...Base::MULTIPART_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/PHP80Test.php b/tests/PHP80Test.php index 74ed3d047..de3bcb029 100644 --- a/tests/PHP80Test.php +++ b/tests/PHP80Test.php @@ -23,6 +23,7 @@ class PHP80Test extends Base ...Base::ENUM_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, + ...Base::MULTIPART_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/languages/php/test.php b/tests/languages/php/test.php index bdae763ae..55d9382ad 100644 --- a/tests/languages/php/test.php +++ b/tests/languages/php/test.php @@ -80,10 +80,10 @@ $response = $general->upload('string', 123, ['string in array'], Payload::fromData($data, 'video/mp4', 'large_file.mp4')); echo "{$response['result']}\n"; -$response = $general->upload('string', 123, ['string in array'], Payload::fromPath(__DIR__ .'/../../resources/file.png')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromPath(__DIR__ . '/../../resources/file.png')); echo "{$response['result']}\n"; -$response = $general->upload('string', 123, ['string in array'], Payload::fromPath(__DIR__ .'/../../resources/large_file.mp4')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromPath(__DIR__ . '/../../resources/large_file.mp4')); echo "{$response['result']}\n"; $response = $general->enum(MockType::FIRST()); @@ -118,6 +118,11 @@ ); echo $url . "\n"; +$response = $general->multipart(); +echo "{$response['x']}\n"; +$hash = md5($response['responseBody']->toBinary()); +echo "{$hash}\n"; + // Query helper tests echo Query::equal('released', [true]) . "\n"; echo Query::equal('title', ['Spiderman', 'Dr. Strange']) . "\n"; @@ -142,13 +147,13 @@ echo Query::contains('title', ['Spider']) . "\n"; echo Query::contains('labels', ['first']) . "\n"; echo Query::or([ - Query::equal('released', [true]), - Query::lessThan('releasedYear', 1990) -]) . "\n"; + Query::equal('released', [true]), + Query::lessThan('releasedYear', 1990) + ]) . "\n"; echo Query::and([ - Query::equal('released', [false]), - Query::greaterThan('releasedYear', 2015) -]) . "\n"; + Query::equal('released', [false]), + Query::greaterThan('releasedYear', 2015) + ]) . "\n"; // Permission & Role helper tests echo Permission::read(Role::any()) . "\n"; From 0953cd369fcafd19a1237b75aded32d6ecbaf3d0 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:42:42 -0400 Subject: [PATCH 07/13] refactor(php): formdata logic to client --- templates/php/src/Client.php.twig | 35 ++++++++++++++++++++++++++++-- templates/php/src/Payload.php.twig | 31 -------------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index c80994145..29a69a804 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -189,11 +189,11 @@ class Client $responseBody = json_decode($responseBody, true); break; } - if ($contentType === 'multipart/form-data') { + if (str_contains($contentType, 'multipart/form-data')) { $matches = []; preg_match('/(?[-]+[\w]+)--/m', $responseBody, $matches); if (isset($matches['boundary'])) { - $responseBody = Payload::handleFormData($matches['boundary'], $responseBody); + $responseBody = self::handleFormData($matches['boundary'], $responseBody); } } if (curl_errno($ch)) { @@ -240,4 +240,35 @@ class Client return $output; } + + public static function handleFormData(string $boundary, mixed $responseBody) + { + $parts = explode($boundary, $responseBody); + $data = []; + foreach ($parts as $part) { + $lines = array_values(array_filter(explode("\r\n", $part))); + $matches = []; + $matched = preg_match('/name="?(?\w+)/s', $part, $matches); + if ($matched) { + array_shift($lines); + if(isset($lines[0]) && $lines[0] === 'Content-Type: application/json'){ + array_shift($lines); + $headers = json_decode(implode($lines), true); + $headers = array_combine( + array_map(fn($header)=> $header['name'], $headers), + array_map(fn($header)=> $header['value'], $headers) + ); + $data[$matches['name']] = $headers; + continue; + } + $data[$matches['name']] = implode("\r\n",$lines) ?? '';; + } + } + + $data['responseStatusCode'] = (int) ($data['responseStatusCode'] ?? ''); + $data['duration'] = ((float) ($data['duration'] ?? '')); + $data['responseBody'] = Payload::fromString($data['responseBody'] ?? ''); + + return $data; + } } diff --git a/templates/php/src/Payload.php.twig b/templates/php/src/Payload.php.twig index 38c631da6..d87075d2a 100644 --- a/templates/php/src/Payload.php.twig +++ b/templates/php/src/Payload.php.twig @@ -77,35 +77,4 @@ class Payload { { return $this->data; } - - public static function handleFormData(string $boundary, mixed $responseBody) - { - $parts = explode($boundary, $responseBody); - $data = []; - foreach ($parts as $part) { - $lines = array_values(array_filter(explode("\r\n", $part))); - $matches = []; - $matched = preg_match('/name="?(?\w+)/s', $part, $matches); - if ($matched) { - array_shift($lines); - if(isset($lines[0]) && $lines[0] === 'Content-Type: application/json'){ - array_shift($lines); - $headers = json_decode(implode($lines), true); - $headers = array_combine( - array_map(fn($header)=> $header['name'], $headers), - array_map(fn($header)=> $header['value'], $headers) - ); - $data[$matches['name']] = $headers; - continue; - } - $data[$matches['name']] = implode("",$lines) ?? '';; - } - } - - $data['responseStatusCode'] = (int) ($data['responseStatusCode'] ?? ''); - $data['duration'] = ((float) $data['duration'] ?? ''); - $data['responseBody'] = self::fromString($data['responseBody'] ?? ''); - - return $data; - } } From f87d2420ba4ca9a0bf5feece92e975b1d1bd8b2b Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:47:10 -0400 Subject: [PATCH 08/13] refactor(php): methods names --- src/SDK/Language/PHP.php | 4 ++-- templates/php/src/Payload.php.twig | 4 ++-- templates/php/tests/Services/ServiceTest.php.twig | 2 +- tests/languages/php/test.php | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index 9108d4ffc..3f8e45aa8 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -354,7 +354,7 @@ public function getParamExample(array $param): string $output .= '[]'; break; case self::TYPE_FILE: - $output .= "Payload::fromPath('file.png')"; + $output .= "Payload::fromFile('file.png')"; break; } } else { @@ -378,7 +378,7 @@ public function getParamExample(array $param): string } break; case self::TYPE_FILE: - $output .= "Payload::fromPath('file.png')"; + $output .= "Payload::fromFile('file.png')"; break; } } diff --git a/templates/php/src/Payload.php.twig b/templates/php/src/Payload.php.twig index d87075d2a..4ea07e2b2 100644 --- a/templates/php/src/Payload.php.twig +++ b/templates/php/src/Payload.php.twig @@ -29,7 +29,7 @@ class Payload { return $this->filename; } - public static function fromPath(string $path, ?string $mimeType = null, ?string $filename = null) + public static function fromFile(string $path, ?string $mimeType = null, ?string $filename = null) { $instance = new Payload(); $instance->path = $path; @@ -39,7 +39,7 @@ class Payload { return $instance; } - public static function fromData(string $data, ?string $mimeType = null, ?string $filename = null) + public static function fromBinary(string $data, ?string $mimeType = null, ?string $filename = null) { $instance = new Payload(); $instance->path = null; diff --git a/templates/php/tests/Services/ServiceTest.php.twig b/templates/php/tests/Services/ServiceTest.php.twig index 1d3e40465..eb78af1c9 100644 --- a/templates/php/tests/Services/ServiceTest.php.twig +++ b/templates/php/tests/Services/ServiceTest.php.twig @@ -34,7 +34,7 @@ final class {{service.name | caseUcfirst}}Test extends TestCase { ->andReturn($data); $response = $this->{{service.name | caseCamel}}->{{method.name | caseCamel}}({%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} - {% if parameter.type == 'object' %}array(){% elseif parameter.type == 'array' %}array(){% elseif parameter.type == 'file' %}Payload::fromData('', "image/png"){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'string' %}"{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}"{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%}{% if not loop.last %},{% endif %}{%~ endfor ~%} + {% if parameter.type == 'object' %}array(){% elseif parameter.type == 'array' %}array(){% elseif parameter.type == 'file' %}Payload::fromBinary('', "image/png"){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'string' %}"{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}"{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%}{% if not loop.last %},{% endif %}{%~ endfor ~%} ); $this->assertSame($data, $response); diff --git a/tests/languages/php/test.php b/tests/languages/php/test.php index 55d9382ad..da7eadac6 100644 --- a/tests/languages/php/test.php +++ b/tests/languages/php/test.php @@ -73,17 +73,17 @@ echo "{$response['result']}\n"; $data = file_get_contents(__DIR__ . '/../../resources/file.png'); -$response = $general->upload('string', 123, ['string in array'], Payload::fromData($data, 'image/png', 'file.png')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromBinary($data, 'image/png', 'file.png')); echo "{$response['result']}\n"; $data = file_get_contents(__DIR__ . '/../../resources/large_file.mp4'); -$response = $general->upload('string', 123, ['string in array'], Payload::fromData($data, 'video/mp4', 'large_file.mp4')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromBinary($data, 'video/mp4', 'large_file.mp4')); echo "{$response['result']}\n"; -$response = $general->upload('string', 123, ['string in array'], Payload::fromPath(__DIR__ . '/../../resources/file.png')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromFile(__DIR__ . '/../../resources/file.png')); echo "{$response['result']}\n"; -$response = $general->upload('string', 123, ['string in array'], Payload::fromPath(__DIR__ . '/../../resources/large_file.mp4')); +$response = $general->upload('string', 123, ['string in array'], Payload::fromFile(__DIR__ . '/../../resources/large_file.mp4')); echo "{$response['result']}\n"; $response = $general->enum(MockType::FIRST()); From 8396e721a7b993e4dabe81595ff864ff5170ecae Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:03:51 -0400 Subject: [PATCH 09/13] feat(all): Adding payload type --- src/SDK/Language.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SDK/Language.php b/src/SDK/Language.php index 496c8ac2b..da6136caf 100644 --- a/src/SDK/Language.php +++ b/src/SDK/Language.php @@ -11,6 +11,7 @@ abstract class Language public const TYPE_ARRAY = 'array'; public const TYPE_OBJECT = 'object'; public const TYPE_FILE = 'file'; + public const TYPE_PAYLOAD = 'payload'; /** * @var array From d45a022a594018658526fa42d13044bd3b33fa48 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:04:19 -0400 Subject: [PATCH 10/13] refactor(php): conditions --- src/SDK/Language/PHP.php | 18 +++++++++--------- templates/php/base/params.twig | 4 ++-- templates/php/base/requests/file.twig | 2 +- templates/php/docs/example.md.twig | 2 +- tests/PHP80Test.php | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index 3f8e45aa8..83b60ccc0 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -259,9 +259,6 @@ public function getTypeName(array $parameter, array $spec = []): string return \ucfirst($parameter['name']); } - if ($parameter['name'] === 'body' && strpos($parameter['description'], 'body of execution') !== false) { - return 'Payload'; - } return match ($parameter['type']) { self::TYPE_STRING => 'string', @@ -270,7 +267,8 @@ public function getTypeName(array $parameter, array $spec = []): string self::TYPE_INTEGER => 'int', self::TYPE_ARRAY, self::TYPE_OBJECT => 'array', - self::TYPE_FILE => 'Payload', + self::TYPE_FILE, + self::TYPE_PAYLOAD => 'Payload', default => $parameter['type'], }; } @@ -353,6 +351,9 @@ public function getParamExample(array $param): string case self::TYPE_OBJECT: $output .= '[]'; break; + case self::TYPE_PAYLOAD: + $output .= "Payload::fromString('')"; + break; case self::TYPE_FILE: $output .= "Payload::fromFile('file.png')"; break; @@ -371,11 +372,10 @@ public function getParamExample(array $param): string $output .= ($example) ? 'true' : 'false'; break; case self::TYPE_STRING: - if ($param['name'] === 'body' && strpos($param['description'], 'body of execution') !== false) { - $output .= "Payload::fromJson([])"; - } else { - $output .= "'{$example}'"; - } + $output .= "'{$example}'"; + break; + case self::TYPE_PAYLOAD: + $output .= "Payload::fromJson([])"; break; case self::TYPE_FILE: $output .= "Payload::fromFile('file.png')"; diff --git a/templates/php/base/params.twig b/templates/php/base/params.twig index 648ccd3d4..20bb50aa3 100644 --- a/templates/php/base/params.twig +++ b/templates/php/base/params.twig @@ -4,7 +4,7 @@ {% if not parameter.required and not parameter.nullable %} if (!is_null(${{ parameter.name | caseCamel | escapeKeyword }})) { - {%~ if method.name | caseLower == "createexecution" and parameter.name == 'body' %} + {%~ if param.type == 'payload' %} $apiParams['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}->toBinary(); {%~ else %} $apiParams['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; @@ -21,7 +21,7 @@ $apiHeaders['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; {%~ endfor %} {%~ if method.name | lower == "createexecution" %} - 'accept' => 'multipart/form-data', + $apiHeaders['accept'] = 'multipart/form-data'; {%~ endif %} {%~ for key, header in method.headers %} $apiHeaders['{{ key }}'] = '{{ header }}'; diff --git a/templates/php/base/requests/file.twig b/templates/php/base/requests/file.twig index 156c83cc7..ed48a0257 100644 --- a/templates/php/base/requests/file.twig +++ b/templates/php/base/requests/file.twig @@ -65,7 +65,7 @@ fseek($handle, $start); $chunk = @fread($handle, Client::CHUNK_SIZE); } else { - $chunk = substr($payload->getData(), $start, Client::CHUNK_SIZE); + $chunk = substr(${{parameter.name}}->getData(), $start, Client::CHUNK_SIZE); } $apiParams['{{ parameter.name }}'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($chunk), $mimeType, $postedName); $apiHeaders['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size - 1) . '/' . $size; diff --git a/templates/php/docs/example.md.twig b/templates/php/docs/example.md.twig index b7a97904e..379703603 100644 --- a/templates/php/docs/example.md.twig +++ b/templates/php/docs/example.md.twig @@ -1,7 +1,7 @@ param.type == 'file') | length > 0 or method.name | caseLower == 'createexecution' %} +{% if method.parameters.all | filter((param) => param.type == 'file') | length > 0 or filter((param) => param.type == 'payload') | length > 0 %} use {{ spec.title | caseUcfirst }}\Payload; {% endif %} use {{ spec.title | caseUcfirst }}\Services\{{ service.name | caseUcfirst }}; diff --git a/tests/PHP80Test.php b/tests/PHP80Test.php index de3bcb029..9efdc4995 100644 --- a/tests/PHP80Test.php +++ b/tests/PHP80Test.php @@ -13,7 +13,7 @@ class PHP80Test extends Base protected string $class = 'Appwrite\SDK\Language\PHP'; protected array $build = []; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app php:8.0-cli-alpine php tests/languages/php/test.php'; + 'docker run --network="mockapi" --rm -v %cd%:/app -w /app php:8.0-cli-alpine php tests/languages/php/test.php'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, From d20adcef9edf40cc7248884d1c60774acb04597d Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:06:31 -0400 Subject: [PATCH 11/13] fix(php): windows command --- tests/PHP80Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHP80Test.php b/tests/PHP80Test.php index 9efdc4995..de3bcb029 100644 --- a/tests/PHP80Test.php +++ b/tests/PHP80Test.php @@ -13,7 +13,7 @@ class PHP80Test extends Base protected string $class = 'Appwrite\SDK\Language\PHP'; protected array $build = []; protected string $command = - 'docker run --network="mockapi" --rm -v %cd%:/app -w /app php:8.0-cli-alpine php tests/languages/php/test.php'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app php:8.0-cli-alpine php tests/languages/php/test.php'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, From f40135bc0e03fa202073a94e0ef11d246dfdd3de Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:11:14 -0400 Subject: [PATCH 12/13] fix(php): wrong condition check --- templates/php/docs/example.md.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/php/docs/example.md.twig b/templates/php/docs/example.md.twig index 379703603..dd805524d 100644 --- a/templates/php/docs/example.md.twig +++ b/templates/php/docs/example.md.twig @@ -1,7 +1,7 @@ param.type == 'file') | length > 0 or filter((param) => param.type == 'payload') | length > 0 %} +{% if method.parameters.all | filter((param) => param.type == 'file' or param.type == 'payload') | length > 0 %} use {{ spec.title | caseUcfirst }}\Payload; {% endif %} use {{ spec.title | caseUcfirst }}\Services\{{ service.name | caseUcfirst }}; From 127730685807b9316da170eea1cae77477be10a2 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:08:19 -0400 Subject: [PATCH 13/13] fix(php): review notes --- mock-server/app/http.php | 2 +- templates/php/base/params.twig | 3 --- templates/php/src/Client.php.twig | 29 ++++++++++++++------- templates/php/src/Payload.php.twig | 10 ++++--- templates/php/src/Services/Service.php.twig | 4 +-- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/mock-server/app/http.php b/mock-server/app/http.php index 3af0eeab6..265385d3c 100644 --- a/mock-server/app/http.php +++ b/mock-server/app/http.php @@ -316,7 +316,7 @@ ->param('x', '', new Text(100), 'Sample string param') ->param('y', '', new Integer(true), 'Sample numeric param') ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') - ->param('payload', [], new File(), 'Sample file param', skipValidation: true) + ->param('file', [], new File(), 'Sample file param', skipValidation: true) ->inject('request') ->inject('response') ->action(function (string $x, int $y, array $z, mixed $file, Request $request, Response $response) { diff --git a/templates/php/base/params.twig b/templates/php/base/params.twig index 20bb50aa3..036ad35d8 100644 --- a/templates/php/base/params.twig +++ b/templates/php/base/params.twig @@ -20,9 +20,6 @@ {%~ for parameter in method.parameters.header %} $apiHeaders['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; {%~ endfor %} - {%~ if method.name | lower == "createexecution" %} - $apiHeaders['accept'] = 'multipart/form-data'; - {%~ endif %} {%~ for key, header in method.headers %} $apiHeaders['{{ key }}'] = '{{ header }}'; {%~ endfor %} diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index 4a54cdc25..4b57bb79d 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -138,6 +138,7 @@ class Client break; case 'multipart/form-data': + $headers['accept'] = 'multipart/form-data'; $query = $this->flatten($params); break; @@ -259,21 +260,31 @@ class Client array_shift($lines); if(isset($lines[0]) && $lines[0] === 'Content-Type: application/json'){ array_shift($lines); - $headers = json_decode(implode($lines), true); - $headers = array_combine( - array_map(fn($header)=> $header['name'], $headers), - array_map(fn($header)=> $header['value'], $headers) - ); - $data[$matches['name']] = $headers; + $json = json_decode(implode($lines), true); + + if (count($json) > 0 && isset($json[0]['name']) && isset($json[0]['value'])) { + $json = array_combine( + array_map(fn($header) => $header['name'], $json), + array_map(fn($header) => $header['value'], $json) + ); + } + + $data[$matches['name']] = $json; continue; } $data[$matches['name']] = implode("\r\n",$lines) ?? '';; } } - $data['responseStatusCode'] = (int) ($data['responseStatusCode'] ?? ''); - $data['duration'] = ((float) ($data['duration'] ?? '')); - $data['responseBody'] = Payload::fromString($data['responseBody'] ?? ''); + if(isset($data['responseStatusCode'])) { + $data['responseStatusCode'] = (int) ($data['responseStatusCode'] ?? ''); + } + if(isset($data['duration'])) { + $data['duration'] = ((float) ($data['duration'] ?? '')); + } + if(isset($data['responseBody'])) { + $data['responseBody'] = Payload::fromString($data['responseBody'] ?? ''); + } return $data; } diff --git a/templates/php/src/Payload.php.twig b/templates/php/src/Payload.php.twig index 865cc50a8..3fcbe6821 100644 --- a/templates/php/src/Payload.php.twig +++ b/templates/php/src/Payload.php.twig @@ -29,7 +29,7 @@ class Payload { return $this->filename; } - public static function fromFile(string $path, ?string $mimeType = null, ?string $filename = null): Payload + public static function fromFile(string $path, ?string $mimeType = null, ?string $filename = null): self { $instance = new Payload(); $instance->path = $path; @@ -39,7 +39,7 @@ class Payload { return $instance; } - public static function fromBinary(string $data, ?string $mimeType = null, ?string $filename = null): Payload + public static function fromBinary(string $data, ?string $mimeType = null, ?string $filename = null): self { $instance = new Payload(); $instance->path = null; @@ -49,14 +49,16 @@ class Payload { return $instance; } - public static function fromJson(array $data) { + public static function fromJson(array $data): self + { $instance = new Payload(); $instance->path = null; $instance->data = json_encode($data); return $instance; } - public static function fromString(string $data) { + public static function fromString(string $data): self + { $instance = new Payload(); $instance->path = null; $instance->data = $data; diff --git a/templates/php/src/Services/Service.php.twig b/templates/php/src/Services/Service.php.twig index 56e96dfe3..4e901e329 100644 --- a/templates/php/src/Services/Service.php.twig +++ b/templates/php/src/Services/Service.php.twig @@ -53,7 +53,7 @@ class {{ service.name | caseUcfirst }} extends Service ); {{~ include('php/base/params.twig') -}} - {%~ if 'multipart/form-data' in method.consumes and method.name | lower != "createexecution" %} + {%~ if 'multipart/form-data' in method.consumes and method.type != "upload" %} {{~ include('php/base/requests/file.twig') }} {%~ else %} @@ -64,4 +64,4 @@ class {{ service.name | caseUcfirst }} extends Service {%~ endif %} {%~ endfor %} -} \ No newline at end of file +}