diff --git a/CHANGELOG.md b/CHANGELOG.md index 2496660..fc7fec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Paginated list models no longer throw a `TypeError` when the gateway returns an entry that is not an object: `webhooks`, `company_statements`, `clients`, `client recurring tokens`, `billing_templates` and `billing template clients` now skip non-array entries instead of failing inside the `fromArray()` closure (`Argument #1 ($w) must be of type array, string given`). +- `account->balance()` and `account->turnover()` return `[]` when the response is not an object, instead of a `TypeError` from the declared `array` return type (`Return value must be of type array, string returned`). + ## [2.1.1] - 2026-10-23 ### Fixed diff --git a/lib/Model/Billing/BillingTemplateClientList.php b/lib/Model/Billing/BillingTemplateClientList.php index 8da146f..830d210 100644 --- a/lib/Model/Billing/BillingTemplateClientList.php +++ b/lib/Model/Billing/BillingTemplateClientList.php @@ -19,8 +19,12 @@ class BillingTemplateClientList implements \JsonSerializable public static function fromArray(array $data): self { $list = new self(); - $list->results = isset($data['results']) && is_array($data['results']) - ? array_map(fn (array $r) => BillingTemplateClient::fromArray($r), $data['results']) + $results = $data['results'] ?? null; + $list->results = is_array($results) + ? array_values(array_map( + fn (array $r) => BillingTemplateClient::fromArray($r), + array_filter($results, 'is_array') + )) : null; $list->next = $data['next'] ?? null; $list->previous = $data['previous'] ?? null; diff --git a/lib/Model/Billing/BillingTemplateList.php b/lib/Model/Billing/BillingTemplateList.php index 4369fc1..c4fa4e5 100644 --- a/lib/Model/Billing/BillingTemplateList.php +++ b/lib/Model/Billing/BillingTemplateList.php @@ -19,8 +19,12 @@ class BillingTemplateList implements \JsonSerializable public static function fromArray(array $data): self { $list = new self(); - $list->results = isset($data['results']) && is_array($data['results']) - ? array_map(fn (array $r) => BillingTemplate::fromArray($r), $data['results']) + $results = $data['results'] ?? null; + $list->results = is_array($results) + ? array_values(array_map( + fn (array $r) => BillingTemplate::fromArray($r), + array_filter($results, 'is_array') + )) : null; $list->next = $data['next'] ?? null; $list->previous = $data['previous'] ?? null; diff --git a/lib/Model/ClientList.php b/lib/Model/ClientList.php index b7c5e98..24a6969 100644 --- a/lib/Model/ClientList.php +++ b/lib/Model/ClientList.php @@ -19,10 +19,13 @@ class ClientList implements \JsonSerializable public static function fromArray(array $data): self { $list = new self(); - $list->results = array_map( - fn (array $c) => ClientDetails::fromArray($c), - $data['results'] ?? [] - ); + $results = $data['results'] ?? []; + $list->results = is_array($results) + ? array_values(array_map( + fn (array $c) => ClientDetails::fromArray($c), + array_filter($results, 'is_array') + )) + : []; $list->next = $data['next'] ?? null; $list->previous = $data['previous'] ?? null; diff --git a/lib/Model/ClientRecurringTokenList.php b/lib/Model/ClientRecurringTokenList.php index d79d784..457464a 100644 --- a/lib/Model/ClientRecurringTokenList.php +++ b/lib/Model/ClientRecurringTokenList.php @@ -19,10 +19,13 @@ class ClientRecurringTokenList implements \JsonSerializable public static function fromArray(array $data): self { $list = new self(); - $list->results = array_map( - fn (array $t) => ClientRecurringToken::fromArray($t), - $data['results'] ?? [] - ); + $results = $data['results'] ?? []; + $list->results = is_array($results) + ? array_values(array_map( + fn (array $t) => ClientRecurringToken::fromArray($t), + array_filter($results, 'is_array') + )) + : []; $list->next = $data['next'] ?? null; $list->previous = $data['previous'] ?? null; diff --git a/lib/Model/CompanyStatementList.php b/lib/Model/CompanyStatementList.php index 30dc2a1..14315ae 100644 --- a/lib/Model/CompanyStatementList.php +++ b/lib/Model/CompanyStatementList.php @@ -19,10 +19,13 @@ class CompanyStatementList implements \JsonSerializable public static function fromArray(array $data): self { $list = new self(); - $list->results = array_map( - fn (array $s) => CompanyStatement::fromArray($s), - $data['results'] ?? [] - ); + $results = $data['results'] ?? []; + $list->results = is_array($results) + ? array_values(array_map( + fn (array $s) => CompanyStatement::fromArray($s), + array_filter($results, 'is_array') + )) + : []; $list->next = $data['next'] ?? null; $list->previous = $data['previous'] ?? null; diff --git a/lib/Model/WebhookList.php b/lib/Model/WebhookList.php index ffbe8ba..d1fdfe9 100644 --- a/lib/Model/WebhookList.php +++ b/lib/Model/WebhookList.php @@ -19,10 +19,13 @@ class WebhookList implements \JsonSerializable public static function fromArray(array $data): self { $list = new self(); - $list->results = array_map( - fn (array $w) => Webhook::fromArray($w), - $data['results'] ?? [] - ); + $results = $data['results'] ?? []; + $list->results = is_array($results) + ? array_values(array_map( + fn (array $w) => Webhook::fromArray($w), + array_filter($results, 'is_array') + )) + : []; $list->next = $data['next'] ?? null; $list->previous = $data['previous'] ?? null; diff --git a/lib/Resource/AccountResource.php b/lib/Resource/AccountResource.php index e7aa1d6..968679a 100644 --- a/lib/Resource/AccountResource.php +++ b/lib/Resource/AccountResource.php @@ -23,8 +23,11 @@ public function balance(array $filters = []): array ]); $json = json_encode($response); + $decoded = json_decode($json !== false ? $json : '[]', true); - return json_decode($json !== false ? $json : '[]', true); + // These endpoints answer with an object; a scalar or plain-text body must + // not turn into a TypeError from the declared array return type. + return is_array($decoded) ? $decoded : []; } /** @@ -38,7 +41,10 @@ public function turnover(array $filters = []): array ]); $json = json_encode($response); + $decoded = json_decode($json !== false ? $json : '[]', true); - return json_decode($json !== false ? $json : '[]', true); + // These endpoints answer with an object; a scalar or plain-text body must + // not turn into a TypeError from the declared array return type. + return is_array($decoded) ? $decoded : []; } } diff --git a/tests/ResponseShapeTest.php b/tests/ResponseShapeTest.php index 656359b..888c93a 100644 --- a/tests/ResponseShapeTest.php +++ b/tests/ResponseShapeTest.php @@ -176,4 +176,38 @@ public function testClientsIterateStopsOnNonStringNext(): void $this->assertSame([], iterator_to_array($api->clients->iterate())); } + + public function testListSkipsNonArrayResults(): void + { + $api = $this->api([new Response(200, [], '{"results": ["x", {"id": "wh_1"}], "next": null, "previous": null}')]); + + $list = $api->webhooks->list(); + + $this->assertIsArray($list->results); + $this->assertCount(1, $list->results); + $this->assertSame('wh_1', $list->results[0]->id); + } + + public function testBillingTemplateListSkipsNonArrayResults(): void + { + $api = $this->api([new Response(200, [], '{"results": ["x"], "next": null, "previous": null}')]); + + $list = $api->billing->listTemplates(); + + $this->assertSame([], $list->results); + } + + public function testBalanceReturnsEmptyArrayForNonObjectBody(): void + { + $api = $this->api([new Response(200, [], '"not-an-object"')]); + + $this->assertSame([], $api->account->balance()); + } + + public function testTurnoverReturnsEmptyArrayForNonObjectBody(): void + { + $api = $this->api([new Response(200, [], '"not-an-object"')]); + + $this->assertSame([], $api->account->turnover()); + } }