diff --git a/codegen/pkg/generator/collect.go b/codegen/pkg/generator/collect.go index be48f87..c8e14f4 100644 --- a/codegen/pkg/generator/collect.go +++ b/codegen/pkg/generator/collect.go @@ -133,7 +133,7 @@ func (g *Generator) registerSchemaUsage(schema *base.SchemaProxy, suggestedName name := g.classNameForSchema(schema) if name == "" { - if suggestedName == "" || !schemaIsObject(schema) || schemaIsAdditionalPropertiesOnly(schema) { + if suggestedName == "" || !schemaShouldGenerateClass(schema) { return "" } name = suggestedName @@ -164,8 +164,8 @@ func (g *Generator) assignSchemasToTags(usage map[string]*schemaUsage) (map[stri for schemaName, info := range usage { targetTag := typesTagKey - // Skip schemas that are additionalProperties-only (they'll be treated as arrays) - if schemaIsAdditionalPropertiesOnly(info.schema) { + // Skip schemas that should remain generic maps in PHP. + if !schemaShouldGenerateClass(info.schema) { continue } @@ -209,7 +209,7 @@ func (g *Generator) inlinePropertyClassName(parentName string, propertyName stri return "" } - if !schemaIsObject(schema) || schemaIsAdditionalPropertiesOnly(schema) { + if !schemaShouldGenerateClass(schema) { return "" } @@ -225,7 +225,7 @@ func (g *Generator) inlineArrayItemClassName(parentName string, schema *base.Sch return "" } - if !schemaIsObject(schema) || schemaIsAdditionalPropertiesOnly(schema) { + if !schemaShouldGenerateClass(schema) { return "" } diff --git a/codegen/pkg/generator/operations.go b/codegen/pkg/generator/operations.go index a16feba..c5bfaf7 100644 --- a/codegen/pkg/generator/operations.go +++ b/codegen/pkg/generator/operations.go @@ -309,6 +309,10 @@ func (g *Generator) buildResponseType(schema *base.SchemaProxy, currentNamespace return g.buildResponseTypeFromSpec(schema.Schema(), currentNamespace) } + if !schemaShouldGenerateClass(schema) { + return &responseType{Kind: responseTypeObject} + } + name := schemaClassName(schema) namespace := g.schemaNamespaces[name] typeName := name @@ -342,7 +346,7 @@ func (g *Generator) buildResponseType(schema *base.SchemaProxy, currentNamespace } } - if hasSchemaType(spec, "object") && inlineBaseName != "" { + if hasSchemaType(spec, "object") && inlineBaseName != "" && schemaShouldGenerateClass(schema) { return &responseType{ Kind: responseTypeClass, ClassName: fmt.Sprintf("\\SumUp\\Services\\%s", inlineBaseName), diff --git a/codegen/pkg/generator/properties.go b/codegen/pkg/generator/properties.go index 78b971e..9e5bcbb 100644 --- a/codegen/pkg/generator/properties.go +++ b/codegen/pkg/generator/properties.go @@ -205,8 +205,7 @@ func (g *Generator) resolvePHPType(schema *base.SchemaProxy, currentNamespace st return g.resolvePHPTypeFromSpec(schema, schema.Schema(), currentNamespace, parentSchemaName, propertyName) } - // Check if this is an additionalProperties-only schema - treat as array - if schemaIsAdditionalPropertiesOnly(schema) { + if !schemaShouldGenerateClass(schema) { return "array", "array" } @@ -267,7 +266,7 @@ func (g *Generator) resolvePHPTypeFromSpec(schema *base.SchemaProxy, spec *base. } return "array", itemDoc + "[]" case hasSchemaType(spec, "object"): - if schema != nil && !schemaIsAdditionalPropertiesOnly(schema) { + if schema != nil && schemaShouldGenerateClass(schema) { typeName := g.classNameForSchema(schema) if typeName == "" && parentSchemaName != "" && propertyName != "" { typeName = phpInlineObjectName(parentSchemaName, propertyName) diff --git a/codegen/pkg/generator/schemas.go b/codegen/pkg/generator/schemas.go index 89e2bda..7eed9aa 100644 --- a/codegen/pkg/generator/schemas.go +++ b/codegen/pkg/generator/schemas.go @@ -39,6 +39,60 @@ func schemaIsAdditionalPropertiesOnly(schema *base.SchemaProxy) bool { return true } +// schemaHasDeclaredProperties reports whether the schema exposes at least one +// explicit property directly or through allOf/oneOf/anyOf composition. +func schemaHasDeclaredProperties(schema *base.SchemaProxy) bool { + return schemaHasDeclaredPropertiesWithStack(schema, make(map[*base.SchemaProxy]struct{})) +} + +// schemaShouldGenerateClass reports whether the schema should become a DTO +// class instead of a generic map. Bare object schemas and +// additionalProperties-only schemas stay as arrays in the PHP SDK. +func schemaShouldGenerateClass(schema *base.SchemaProxy) bool { + return schemaIsObject(schema) && schemaHasDeclaredProperties(schema) && !schemaIsAdditionalPropertiesOnly(schema) +} + +func schemaHasDeclaredPropertiesWithStack(schema *base.SchemaProxy, stack map[*base.SchemaProxy]struct{}) bool { + if schema == nil { + return false + } + + if _, ok := stack[schema]; ok { + return false + } + stack[schema] = struct{}{} + defer delete(stack, schema) + + spec := schema.Schema() + if spec == nil { + return false + } + + if spec.Properties != nil && spec.Properties.Len() > 0 { + return true + } + + for _, composite := range spec.AllOf { + if schemaHasDeclaredPropertiesWithStack(composite, stack) { + return true + } + } + + for _, composite := range spec.AnyOf { + if schemaHasDeclaredPropertiesWithStack(composite, stack) { + return true + } + } + + for _, composite := range spec.OneOf { + if schemaHasDeclaredPropertiesWithStack(composite, stack) { + return true + } + } + + return false +} + func schemaIsObjectWithStack(schema *base.SchemaProxy, stack map[*base.SchemaProxy]struct{}) bool { if schema == nil { return false diff --git a/codegen/pkg/generator/services.go b/codegen/pkg/generator/services.go index cdc955a..449561b 100644 --- a/codegen/pkg/generator/services.go +++ b/codegen/pkg/generator/services.go @@ -453,7 +453,7 @@ func shouldGenerateRequestBodyClass(op *operation) bool { return false } - return !schemaIsAdditionalPropertiesOnly(op.BodySchema) + return schemaShouldGenerateClass(op.BodySchema) } func buildEmptyRequestBodyClass(className string) string { diff --git a/src/Checkouts/Checkouts.php b/src/Checkouts/Checkouts.php index 9b6f894..e4b9f8f 100644 --- a/src/Checkouts/Checkouts.php +++ b/src/Checkouts/Checkouts.php @@ -42,10 +42,6 @@ public function __construct(array $data = []) } -class CheckoutsCreateApplePaySessionResponse -{ -} - class CheckoutsListAvailablePaymentMethodsResponse { /** @@ -194,13 +190,13 @@ public function create(\SumUp\Types\CheckoutCreateRequest|array $body, ?RequestO * @param CheckoutsCreateApplePaySessionRequest|array|null $body Optional request payload * @param RequestOptions|null $requestOptions Optional typed request options * - * @return \SumUp\Services\CheckoutsCreateApplePaySessionResponse + * @return array * @throws \SumUp\Exception\ApiException * @throws \SumUp\Exception\UnexpectedApiException * @throws \SumUp\Exception\ConnectionException * @throws \SumUp\Exception\SDKException */ - public function createApplePaySession(string $id, CheckoutsCreateApplePaySessionRequest|array|null $body = null, ?RequestOptions $requestOptions = null): \SumUp\Services\CheckoutsCreateApplePaySessionResponse + public function createApplePaySession(string $id, CheckoutsCreateApplePaySessionRequest|array|null $body = null, ?RequestOptions $requestOptions = null): array { $path = sprintf('/v0.2/checkouts/%s/apple-pay-session', rawurlencode((string) $id)); $payload = []; @@ -213,7 +209,9 @@ public function createApplePaySession(string $id, CheckoutsCreateApplePaySession $response = $this->client->send('PUT', $path, $payload, $headers, $requestOptions); - return ResponseDecoder::decodeOrThrow($response, \SumUp\Services\CheckoutsCreateApplePaySessionResponse::class, [ + return ResponseDecoder::decodeOrThrow($response, [ + '200' => ['type' => 'object'], + ], [ '400' => ['type' => 'mixed'], '404' => ['type' => 'class', 'class' => \SumUp\Types\Error::class], ], 'PUT', $path); diff --git a/src/ResponseDecoder.php b/src/ResponseDecoder.php index daf8588..5a0c5db 100644 --- a/src/ResponseDecoder.php +++ b/src/ResponseDecoder.php @@ -135,7 +135,15 @@ private static function castValue($value, array $descriptor) case 'scalar': return self::castScalar($value, isset($descriptor['scalar']) ? $descriptor['scalar'] : 'mixed'); case 'object': - return is_array($value) ? $value : []; + if (is_array($value)) { + return $value; + } + + if (is_object($value)) { + return get_object_vars($value); + } + + return []; case 'void': return null; case 'mixed': diff --git a/src/Types/ProcessCheckout.php b/src/Types/ProcessCheckout.php index 14695ed..918b4c6 100644 --- a/src/Types/ProcessCheckout.php +++ b/src/Types/ProcessCheckout.php @@ -40,16 +40,16 @@ class ProcessCheckout /** * Raw `PaymentData` object received from Google Pay. Send the Google Pay response payload as-is. * - * @var ProcessCheckoutGooglePay|null + * @var array|null */ - public ?ProcessCheckoutGooglePay $googlePay = null; + public ?array $googlePay = null; /** * Raw payment token object received from Apple Pay. Send the Apple Pay response payload as-is. * - * @var ProcessCheckoutApplePay|null + * @var array|null */ - public ?ProcessCheckoutApplePay $applePay = null; + public ?array $applePay = null; /** * Saved-card token to use instead of raw card details when processing with a previously stored payment instrument. diff --git a/src/Types/ProcessCheckoutApplePay.php b/src/Types/ProcessCheckoutApplePay.php deleted file mode 100644 index 6288f10..0000000 --- a/src/Types/ProcessCheckoutApplePay.php +++ /dev/null @@ -1,12 +0,0 @@ -|null */ - public ?ReceiptEmvData $emvData = null; + public ?array $emvData = null; /** * Acquirer-specific metadata related to the card authorization. diff --git a/src/Types/ReceiptEmvData.php b/src/Types/ReceiptEmvData.php deleted file mode 100644 index 075a928..0000000 --- a/src/Types/ReceiptEmvData.php +++ /dev/null @@ -1,12 +0,0 @@ -assertSame(MandateResponseStatus::ACTIVE, $checkout->mandate->status); } + public function testHydrateOpaqueObjectSchemaAsArray() + { + $checkout = Hydrator::hydrate([ + 'apple_pay' => [ + 'token' => [ + 'paymentData' => [ + 'version' => 'EC_v1', + ], + ], + ], + ], ProcessCheckout::class); + + $this->assertInstanceOf(ProcessCheckout::class, $checkout); + $this->assertIsArray($checkout->applePay); + $this->assertSame('EC_v1', $checkout->applePay['token']['paymentData']['version']); + } + public function testHydrateArrayItemsFromDocblockClassNames() { $response = Hydrator::hydrate([ diff --git a/tests/ResponseDecoderTest.php b/tests/ResponseDecoderTest.php index b4a7246..d7fbe05 100644 --- a/tests/ResponseDecoderTest.php +++ b/tests/ResponseDecoderTest.php @@ -70,6 +70,28 @@ public function testDecodeOrThrowDecodesSuccessArrayDescriptorWithClassItems() $this->assertSame('ONE', $result[0]->errorCode); } + public function testDecodeOrThrowDecodesOpaqueObjectDescriptorFromStdClass() + { + $response = new Response(200, (object) [ + 'merchantSessionIdentifier' => 'session_123', + 'nested' => (object) ['value' => 'ok'], + ]); + + $result = ResponseDecoder::decodeOrThrow( + $response, + [ + '200' => ['type' => 'object'], + ], + null, + 'POST', + '/v0.1/checkouts/chk_123/create_apple_pay_session' + ); + + $this->assertIsArray($result); + $this->assertSame('session_123', $result['merchantSessionIdentifier']); + $this->assertInstanceOf(\stdClass::class, $result['nested']); + } + public function testDecodeOrThrowUsesUnexpectedFallbackForUnknownStatusWithNestedPayload() { $rawBody = [