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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [11.3.0] - 2026-09-28
### Fixed
These affect `CLIENT_PHP_VERSION` 8.1 and later, where enums are generated as native PHP enums.
- Enum case names that start with a digit (e.g. the value `+7 days`) are now prefixed with `V_`, as numeric values already were (`case V_7_DAYS`). They used to produce an invalid `case 7_DAYS` that aborted the generation
- Optional and nullable array-of-enum request parameters are now only mapped to their values when set. Leaving one unset used to throw a `TypeError` from `array_map()` when the request was sent
- Enum header parameters are now sent as their values, like query, path and cookie parameters. The enum object itself used to reach the PSR-7 message, which threw an `InvalidArgumentException`

### Changed
- Inline array-of-enum items now get their own enum per operation and field, `{Operation}{Field}ItemEnum` (e.g. `FindServicesCriterionStatusInItemEnum`), following the naming of inline array-of-object items. They used to share a single `Schema\Enum` class, which held the values of only one of them. Regenerating a client that already contains `Schema\Enum` renames that class

## [11.2.3] - 2026-09-28
### Fixed
- Generated clients can no longer resolve dependency versions that raise PHP 8.4+ deprecations: the generated `composer.json` now requires `docler-labs/api-client-exception` `^1.0 || ^2.3.1` (was `^1.0 || ^2.0`) and, for the Guzzle HTTP message implementation, `guzzlehttp/psr7` `^1.6 || ^2.7` (was `^1.6 || ^2.6`). The 1.x lines stay allowed, so every `CLIENT_PHP_VERSION` target keeps resolving; on PHP 7.4+ Composer now picks only releases that run deprecation-free on PHP 8.5
Expand Down
6 changes: 3 additions & 3 deletions src/Generator/EnumGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ class EnumGenerator extends MutatorAccessorClassGeneratorAbstract

public static function getCaseName(string $value): string
{
$sanitized = preg_replace('/[^A-Z0-9_]/', '', strtoupper(str_replace([' ', '-', '/', '.'], '_', $value)));
$sanitized = (string)preg_replace('/[^A-Z0-9_]/', '', strtoupper(str_replace([' ', '-', '/', '.'], '_', $value)));

if (is_numeric($sanitized)) {
if (preg_match('/^[0-9]/', $sanitized) === 1) {
return 'V_' . $sanitized;
}

return (string)$sanitized;
return $sanitized;
}

public function generate(Specification $specification, PhpFileCollection $fileRegistry): void
Expand Down
17 changes: 12 additions & 5 deletions src/Generator/RequestGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ private function generateFieldsArray(array $fields): array
} elseif ($field->isArrayOfEnums()) {
$enumField = $field->getArrayItem();
$this->addImport($this->fqdn($this->withSubNamespace(SchemaGenerator::NAMESPACE_SUBPATH), $enumField->getPhpClassName()));
$fieldsArr[$field->getName()] = $this->builder->funcCall(
$enumValues = $this->builder->funcCall(
'array_map',
[
$this->builder->arrowFunction(
Expand All @@ -442,6 +442,16 @@ private function generateFieldsArray(array $fields): array
$fieldsArr[$field->getName()],
]
);

if ($field->isNullable() || $field->isOptional()) {
$enumValues = $this->builder->ternary(
$this->builder->equals($fieldsArr[$field->getName()], $this->builder->val(null)),
$this->builder->val(null),
$enumValues
);
}

$fieldsArr[$field->getName()] = $enumValues;
}
}
}
Expand Down Expand Up @@ -490,11 +500,8 @@ private function generateGetHeadersMethod(
}

$returnVal = $this->builder->array($headers);
$fieldsArr = [];
$fieldsArr = $this->generateFieldsArray($fields);
$returnType = 'array';
foreach ($fields as $field) {
$fieldsArr[$field->getName()] = $this->builder->localPropertyFetch($field->getPhpVariableName());
}

if (!empty($fieldsArr)) {
$returnVal = $this->builder->funcCall(
Expand Down
6 changes: 5 additions & 1 deletion src/Input/Factory/FieldFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ public function create(
}
$sibling = $this->resolveReference($schema->items);
$itemsReference = $schema->items;
if (isset($sibling->allOf) || FieldType::isSpecificationTypeObject($sibling->type)) {
if (
isset($sibling->allOf)
|| FieldType::isSpecificationTypeObject($sibling->type)
|| (!($itemsReference instanceof Reference) && !empty($sibling->enum))
) {
$itemReferenceName = SchemaNaming::getClassName(
$itemsReference,
$operationName . ucfirst($fieldName) . 'Item'
Expand Down
6 changes: 6 additions & 0 deletions test/suite/functional/Generator/EnumGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public function exampleProvider(): array
self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\IntParamEnum',
ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(),
],
'Inline array of enums request parameter, value starting with a digit' => [
'/Request/getResourcesByStatuses.yaml',
'/Schema/GetResourcesByStatusesStatusesItemEnum.php',
self::BASE_NAMESPACE . SchemaGenerator::NAMESPACE_SUBPATH . '\\GetResourcesByStatusesStatusesItemEnum',
ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(),
],
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

/*
* This file was generated by docler-labs/api-client-generator.
*
* Do not edit it manually.
*/

namespace Test\Request;

use Test\Schema\SerializableInterface;

class GetResourcesByStatusesRequest implements RequestInterface
{
public const TYPES_BASIC = 'basic';

public const TYPES_PREMIUM = 'premium';

public const STATUSES_ACTIVE = 'active';

public const STATUSES_INACTIVE = 'inactive';

public const STATUSES_7_DAYS = '+7 days';

private array $types;

private ?array $statuses = null;

private string $contentType = '';

public function __construct(array $types)
{
$this->types = $types;
}

public function getContentType(): string
{
return $this->contentType;
}

/**
* @param string[] $statuses
*/
public function setStatuses(array $statuses): self
{
$this->statuses = $statuses;

return $this;
}

public function getMethod(): string
{
return 'GET';
}

public function getRoute(): string
{
return 'v1/resources-by-statuses';
}

public function getQueryParameters(): array
{
return array_map(static function ($value) {
return $value instanceof SerializableInterface ? $value->toArray() : $value;
}, array_filter(['types' => $this->types, 'statuses' => $this->statuses], static function ($value) {
return null !== $value;
}));
}

public function getRawQueryParameters(): array
{
return ['types' => $this->types, 'statuses' => $this->statuses];
}

public function getCookies(): array
{
return [];
}

public function getHeaders(): array
{
return [];
}

public function getBody()
{
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types=1);

/*
* This file was generated by docler-labs/api-client-generator.
*
* Do not edit it manually.
*/

namespace Test\Request;

use Test\Schema\GetResourcesByStatusesStatusesItemEnum;
use Test\Schema\GetResourcesByStatusesTypesItemEnum;
use Test\Schema\SerializableInterface;

class GetResourcesByStatusesRequest implements RequestInterface
{
private ?array $statuses = null;

private string $contentType = '';

public function __construct(private readonly array $types)
{
}

public function getContentType(): string
{
return $this->contentType;
}

/**
* @param GetResourcesByStatusesStatusesItemEnum[] $statuses
*/
public function setStatuses(array $statuses): self
{
$this->statuses = $statuses;

return $this;
}

public function getMethod(): string
{
return 'GET';
}

public function getRoute(): string
{
return 'v1/resources-by-statuses';
}

public function getQueryParameters(): array
{
return array_map(static function ($value) {
return $value instanceof SerializableInterface ? $value->toArray() : $value;
}, array_filter(['types' => array_map(static fn (GetResourcesByStatusesTypesItemEnum $enum): string => $enum->value, $this->types), 'statuses' => $this->statuses === null ? null : array_map(static fn (GetResourcesByStatusesStatusesItemEnum $enum): string => $enum->value, $this->statuses)], static function ($value) {
return null !== $value;
}));
}

public function getRawQueryParameters(): array
{
return ['types' => array_map(static fn (GetResourcesByStatusesTypesItemEnum $enum): string => $enum->value, $this->types), 'statuses' => $this->statuses === null ? null : array_map(static fn (GetResourcesByStatusesStatusesItemEnum $enum): string => $enum->value, $this->statuses)];
}

public function getCookies(): array
{
return [];
}

public function getHeaders(): array
{
return [];
}

public function getBody()
{
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types=1);

/*
* This file was generated by docler-labs/api-client-generator.
*
* Do not edit it manually.
*/

namespace Test\Request;

use Test\Schema\GetResourcesByStatusesStatusesItemEnum;
use Test\Schema\GetResourcesByStatusesTypesItemEnum;
use Test\Schema\SerializableInterface;

class GetResourcesByStatusesRequest implements RequestInterface
{
private ?array $statuses = null;

private string $contentType = '';

public function __construct(private readonly array $types)
{
}

public function getContentType(): string
{
return $this->contentType;
}

/**
* @param GetResourcesByStatusesStatusesItemEnum[] $statuses
*/
public function setStatuses(array $statuses): self
{
$this->statuses = $statuses;

return $this;
}

public function getMethod(): string
{
return 'GET';
}

public function getRoute(): string
{
return 'v1/resources-by-statuses';
}

public function getQueryParameters(): array
{
return array_map(static function ($value) {
return $value instanceof SerializableInterface ? $value->toArray() : $value;
}, array_filter(['types' => array_map(static fn (GetResourcesByStatusesTypesItemEnum $enum): string => $enum->value, $this->types), 'statuses' => $this->statuses === null ? null : array_map(static fn (GetResourcesByStatusesStatusesItemEnum $enum): string => $enum->value, $this->statuses)], static function ($value) {
return null !== $value;
}));
}

public function getRawQueryParameters(): array
{
return ['types' => array_map(static fn (GetResourcesByStatusesTypesItemEnum $enum): string => $enum->value, $this->types), 'statuses' => $this->statuses === null ? null : array_map(static fn (GetResourcesByStatusesStatusesItemEnum $enum): string => $enum->value, $this->statuses)];
}

public function getCookies(): array
{
return [];
}

public function getHeaders(): array
{
return [];
}

public function getBody()
{
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function getHeaders(): array
{
return array_merge(['X-API-KEY' => $this->apiKey, 'Content-Type' => $this->contentType], array_map(static function ($value) {
return $value instanceof SerializableInterface ? $value->toArray() : $value;
}, array_filter(['Accept' => $this->accept], static function ($value) {
}, array_filter(['Accept' => $this->accept->value], static function ($value) {
return null !== $value;
})));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function getHeaders(): array
{
return array_merge(['X-API-KEY' => $this->apiKey, 'Content-Type' => $this->contentType], array_map(static function ($value) {
return $value instanceof SerializableInterface ? $value->toArray() : $value;
}, array_filter(['Accept' => $this->accept], static function ($value) {
}, array_filter(['Accept' => $this->accept->value], static function ($value) {
return null !== $value;
})));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public function getHeaders(): array

return array_merge(['X-WSSE' => $xwsse, 'Content-Type' => $this->contentType->value], array_map(static function ($value) {
return $value instanceof SerializableInterface ? $value->toArray() : $value;
}, array_filter(['contentType' => $this->contentType, 'X-Request-ID' => $this->xRequestId], static function ($value) {
}, array_filter(['contentType' => $this->contentType->value, 'X-Request-ID' => $this->xRequestId], static function ($value) {
return null !== $value;
})));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public function getHeaders(): array

return array_merge(['X-WSSE' => $xwsse, 'Content-Type' => $this->contentType->value], array_map(static function ($value) {
return $value instanceof SerializableInterface ? $value->toArray() : $value;
}, array_filter(['contentType' => $this->contentType, 'X-Request-ID' => $this->xRequestId], static function ($value) {
}, array_filter(['contentType' => $this->contentType->value, 'X-Request-ID' => $this->xRequestId], static function ($value) {
return null !== $value;
})));
}
Expand Down
Loading
Loading