Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
run: composer install --prefer-dist --no-progress --no-interaction

- name: Lint
run: make fmtcheck
run: vendor/bin/php-cs-fixer fix -v --using-cache=no --dry-run

static-analysis:
name: Static Analysis
Expand All @@ -51,7 +51,7 @@ jobs:
run: composer install --prefer-dist --no-progress --no-interaction

- name: Run PHPStan
run: make analyse
run: PHPSTAN_DISABLE_PARALLEL=1 vendor/bin/phpstan analyse --configuration=phpstan.neon --no-progress --memory-limit=1G --debug

docs:
name: Docs
Expand All @@ -70,7 +70,7 @@ jobs:
run: composer install --no-interaction --prefer-dist

- name: Generate docs
run: make docs
run: docker run --rm -v "$PWD:/data" "phpdoc/phpdoc:3"

- name: Ensure docs are up to date
run: git diff --exit-code -- docs
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/generate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
working-directory: codegen

- name: Format
run: make fmt
run: vendor/bin/php-cs-fixer fix -v --using-cache=no

- name: Create GitHub App token
id: app-token
Expand Down
40 changes: 0 additions & 40 deletions Makefile

This file was deleted.

14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ try {
// SDK automatically uses SUMUP_API_KEY environment variable
$sumup = new \SumUp\SumUp();

$request = new \SumUp\Types\CheckoutCreateRequest([
'amount' => 10.00,
'currency' => 'EUR', // or CheckoutCreateRequestCurrency::EUR
'checkout_reference' => 'your-checkout-ref',
'merchant_code' => 'YOUR-MERCHANT-CODE',
]);
$request = new \SumUp\Types\CheckoutCreateRequest(
checkoutReference: 'your-checkout-ref',
amount: 10.00,
currency: 'EUR', // or CheckoutCreateRequestCurrency::EUR
merchantCode: 'YOUR-MERCHANT-CODE',
);

$checkout = $sumup->checkouts()->create($request);

Expand All @@ -63,7 +63,7 @@ try {
}
```

Service methods also accept associative arrays as request payloads. For typed usage, prefer DTOs from `\SumUp\Types\...` with `new TypeName([...])`.
Service methods also accept associative arrays as request payloads. For typed usage, prefer DTOs from `\SumUp\Types\...` with named arguments, or use `TypeName::fromArray([...])` when you already have associative array data.

### Providing API Key Programmatically

Expand Down
206 changes: 199 additions & 7 deletions codegen/pkg/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ type Generator struct {

// enumNamespaces tracks where an enum is defined so we can reference it.
enumNamespaces map[string]string

// requestClassNames tracks schema classes used as OpenAPI request bodies.
requestClassNames map[string]struct{}
}

type enumDefinition struct {
Expand All @@ -67,6 +70,7 @@ func New(cfg Config) *Generator {
return &Generator{
cfg: cfg,
inlineSchemaNames: make(map[*base.SchemaProxy]string),
requestClassNames: make(map[string]struct{}),
}
}

Expand All @@ -88,6 +92,7 @@ func (g *Generator) Load(spec *v3.Document) error {
usage := g.collectSchemaUsage()
g.schemasByTag, g.schemaNamespaces = g.assignSchemasToTags(usage)
g.operationsByTag = g.collectOperations()
g.collectRequestClassNames()
g.enumsByTag, g.enumNamespaces = g.collectEnums()

return nil
Expand Down Expand Up @@ -280,32 +285,219 @@ func (g *Generator) buildPHPClass(name string, schema *base.SchemaProxy, current
buf.WriteString(propCode)
}

if strings.HasSuffix(name, "Request") && name != "BadRequest" {
buf.WriteString(g.buildRequestArrayConstructor(properties))
if g.shouldGenerateConstructorForClass(name) {
buf.WriteString(g.buildRequestConstructor(properties))
}

buf.WriteString("}\n")
return buf.String()
}

func (g *Generator) buildRequestArrayConstructor(_ []phpProperty) string {
func (g *Generator) buildRequestConstructor(properties []phpProperty) string {
var buf strings.Builder

buf.WriteString(" /**\n")
buf.WriteString(" * Create request DTO.\n")
buf.WriteString(" *\n")
for _, prop := range constructorProperties(properties) {
fmt.Fprintf(&buf, " * @param %s $%s\n", g.constructorParamDocType(prop), prop.Name)
}
buf.WriteString(" */\n")
buf.WriteString(" public function __construct(")
constructorProps := constructorProperties(properties)
if len(constructorProps) > 0 {
buf.WriteString("\n")
for idx, prop := range constructorProps {
buf.WriteString(" ")
buf.WriteString(g.constructorParamType(prop))
buf.WriteString(" $")
buf.WriteString(prop.Name)
if prop.Optional {
buf.WriteString(" = null")
}
if idx < len(constructorProps)-1 {
buf.WriteString(",")
}
buf.WriteString("\n")
}
buf.WriteString(" ")
}
buf.WriteString(") {\n")
buf.WriteString(" \\SumUp\\Hydrator::hydrate([\n")
for _, prop := range constructorProps {
fmt.Fprintf(&buf, " '%s' => $%s,\n", prop.SerializedName, prop.Name)
}
buf.WriteString(" ], self::class, $this);\n")
buf.WriteString(" }\n\n")

buf.WriteString(" /**\n")
buf.WriteString(" * Create request DTO from an associative array.\n")
buf.WriteString(" *\n")
buf.WriteString(" * @param array<string, mixed> $data\n")
buf.WriteString(" */\n")
buf.WriteString(" public function __construct(array $data = [])\n")
buf.WriteString(" public static function fromArray(array $data): self\n")
buf.WriteString(" {\n")
buf.WriteString(" if ($data !== []) {\n")
buf.WriteString(" \\SumUp\\Hydrator::hydrate($data, self::class, $this);\n")
buf.WriteString(" }\n")
requiredProps := requiredProperties(properties)
if len(requiredProps) > 0 {
buf.WriteString(" self::assertRequiredFields($data, [\n")
for _, prop := range requiredProps {
fmt.Fprintf(&buf, " '%s' => '%s',\n", prop.SerializedName, prop.Name)
}
buf.WriteString(" ]);\n\n")
}
buf.WriteString(" $request = (new \\ReflectionClass(self::class))->newInstanceWithoutConstructor();\n")
buf.WriteString(" \\SumUp\\Hydrator::hydrate($data, self::class, $request);\n\n")
buf.WriteString(" return $request;\n")
buf.WriteString(" }\n\n")

if len(requiredProps) > 0 {
buf.WriteString(" /**\n")
buf.WriteString(" * @param array<string, mixed> $data\n")
buf.WriteString(" * @param array<string, string> $requiredFields\n")
buf.WriteString(" */\n")
buf.WriteString(" private static function assertRequiredFields(array $data, array $requiredFields): void\n")
buf.WriteString(" {\n")
buf.WriteString(" foreach ($requiredFields as $serializedName => $propertyName) {\n")
buf.WriteString(" if (!array_key_exists($serializedName, $data) && !array_key_exists($propertyName, $data)) {\n")
buf.WriteString(" throw new \\InvalidArgumentException(sprintf('Missing required field \"%s\".', $serializedName));\n")
buf.WriteString(" }\n")
buf.WriteString(" }\n")
buf.WriteString(" }\n\n")
}

return buf.String()
}

func constructorProperties(properties []phpProperty) []phpProperty {
result := make([]phpProperty, 0, len(properties))
for _, prop := range properties {
if !prop.Optional {
result = append(result, prop)
}
}
for _, prop := range properties {
if prop.Optional {
result = append(result, prop)
}
}
return result
}

func requiredProperties(properties []phpProperty) []phpProperty {
result := make([]phpProperty, 0, len(properties))
for _, prop := range properties {
if !prop.Optional {
result = append(result, prop)
}
}
return result
}

func (g *Generator) constructorParamDocType(prop phpProperty) string {
docType := prop.DocType
if docType == "" {
docType = "mixed"
}
if backingType := g.enumBackingType(prop.Type); backingType != "" {
docType += "|" + backingType
}
if prop.Optional && !strings.Contains(docType, "null") {
docType += "|null"
}
return docType
}

func (g *Generator) constructorParamType(prop phpProperty) string {
paramType := prop.Type
if paramType == "" {
paramType = "mixed"
}

if backingType := g.enumBackingType(paramType); backingType != "" {
paramType += "|" + backingType
}

if prop.Optional && paramType != "mixed" {
if strings.Contains(paramType, "|") {
if !strings.Contains(paramType, "null") {
paramType += "|null"
}
} else if !strings.HasPrefix(paramType, "?") {
paramType = "?" + paramType
}
}

return paramType
}

func (g *Generator) enumBackingType(typeName string) string {
if typeName == "" {
return ""
}

trimmed := strings.TrimPrefix(typeName, "\\")
parts := strings.Split(trimmed, "\\")
enumName := parts[len(parts)-1]
if enumName == "" {
return ""
}

for _, enums := range g.enumsByTag {
for _, enum := range enums {
if enum.Name == enumName {
return enum.Type
}
}
}

return ""
}

func (g *Generator) collectRequestClassNames() {
g.requestClassNames = make(map[string]struct{})
for _, operations := range g.operationsByTag {
for _, op := range operations {
if op == nil || !op.HasBody || op.BodyType == "" {
continue
}
className := phpClassBaseName(op.BodyType)
if className == "" || isBuiltinPHPType(className) {
continue
}
g.requestClassNames[className] = struct{}{}
}
}
}

func (g *Generator) shouldGenerateConstructorForClass(className string) bool {
if className == "" || className == "BadRequest" {
return false
}
if strings.HasSuffix(className, "Request") {
return true
}
_, ok := g.requestClassNames[className]
return ok
}

func phpClassBaseName(typeName string) string {
trimmed := strings.TrimPrefix(typeName, "\\")
if strings.Contains(trimmed, "|") {
return ""
}
parts := strings.Split(trimmed, "\\")
return parts[len(parts)-1]
}

func isBuiltinPHPType(typeName string) bool {
switch typeName {
case "array", "mixed", "string", "int", "float", "bool", "null", "void":
return true
default:
return false
}
}

func (g *Generator) displayTagName(tagKey string) string {
if tagKey == sharedTagKey {
return sharedTagDisplayName
Expand Down
Loading
Loading