Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
66267fd
feat(php-api-client-base): scaffold package
pepamartinec Jun 9, 2026
a2a686e
feat(php-api-client-base): add Json helper
pepamartinec Jun 9, 2026
39afcad
feat(php-api-client-base): add ResponseModelInterface
pepamartinec Jun 9, 2026
e163e93
feat(php-api-client-base): add ClientException
pepamartinec Jun 9, 2026
f0843b0
fix(php-api-client-base): add phpcs.xml (Keboola standard) and satisf…
pepamartinec Jun 9, 2026
1f6710f
feat(php-api-client-base): add RequestAuthenticatorInterface
pepamartinec Jun 9, 2026
fe1780d
feat(php-api-client-base): add StorageApiTokenAuthenticator
pepamartinec Jun 9, 2026
7a4170c
feat(php-api-client-base): add ManageApiTokenAuthenticator
pepamartinec Jun 9, 2026
19de150
feat(php-api-client-base): add KeboolaServiceAccountAuthenticator
pepamartinec Jun 9, 2026
cd4c047
fix(php-api-client-base): suppress phpstan non-empty-string warnings …
pepamartinec Jun 9, 2026
fa84b1c
feat(php-api-client-base): add RetryDecider with configurable retryab…
pepamartinec Jun 9, 2026
a3c24fd
feat(php-api-client-base): add ApiClientConfiguration
pepamartinec Jun 9, 2026
e206a99
feat(php-api-client-base): add ApiClient
pepamartinec Jun 9, 2026
e7021cd
fix(php-api-client-base): re-run auth inside retry loop; harden Retry…
pepamartinec Jun 9, 2026
166996e
docs(php-api-client-base): add README framed for Keboola service clients
pepamartinec Jun 9, 2026
1aae1bf
ci(php-api-client-base): register lib for tests in monorepo pipeline
pepamartinec Jun 9, 2026
4a79d06
refactor(php-api-client-base): rename ApiClientConfiguration to ApiCl…
pepamartinec Jun 9, 2026
523ade6
refactor(php-api-client-base): make authenticator a first-class ApiCl…
pepamartinec Jun 9, 2026
c29d762
feat(php-api-client-base): require authenticator on ApiClient; add No…
pepamartinec Jun 9, 2026
9fc407b
refactor(php-api-client-base): make errorMessageResolver and retryabl…
pepamartinec Jun 9, 2026
861a1dd
feat(php-api-client-base): make error-message resolver a typed ErrorM…
pepamartinec Jun 10, 2026
4203c54
refactor(php-api-client-base): make ResponseModelInterface::fromRespo…
pepamartinec Jun 10, 2026
7d2ed51
feat(php-api-client-base): expose ApiClientOptions default constants …
pepamartinec Jun 10, 2026
fc81dac
feat(php-api-client-base): make ApiClientOptions logger nullable; coa…
pepamartinec Jun 10, 2026
344cb34
ci(php-api-client-base): add dev-php-api-client-base docker-compose s…
pepamartinec Jun 11, 2026
cb1352c
fix(php-api-client-base): route authenticator failures through retry …
pepamartinec Jun 15, 2026
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
11 changes: 11 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ stages:
configurationVariablesResolver:libs/configuration-variables-resolver \
doctrineRetryBundle:libs/doctrine-retry-bundle \
gitServiceApiClient:libs/git-service-api-client \
phpApiClientBase:libs/php-api-client-base \
inputMapping:libs/input-mapping \
k8sClient:libs/k8s-client \
keyGenerator:libs/key-generator \
Expand Down Expand Up @@ -191,6 +192,14 @@ stages:
jobs:
- template: libs/git-service-api-client/azure-pipelines.tests.yml

- stage: tests_phpApiClientBase
displayName: Tests - PHP API Client Base
lockBehavior: sequential
dependsOn: build
condition: and(succeeded(), dependencies.build.outputs['checkChanges.findChanges.changedProjects_phpApiClientBase'])
jobs:
- template: libs/php-api-client-base/azure-pipelines.tests.yml

- stage: tests_inputMapping
displayName: Tests - Input Mapping
lockBehavior: sequential
Expand Down Expand Up @@ -336,6 +345,7 @@ stages:
- tests_configurationVariablesResolver
- tests_doctrineRetryBundle
- tests_gitServiceApiClient
- tests_phpApiClientBase
- tests_inputMapping
- tests_k8sClient
- tests_keyGenerator
Expand All @@ -360,6 +370,7 @@ stages:
in(dependencies.tests_configurationVariablesResolver.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_doctrineRetryBundle.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_gitServiceApiClient.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_phpApiClientBase.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_inputMapping.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_k8sClient.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_keyGenerator.result, 'Succeeded', 'Skipped'),
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ services:
image: keboola/git-service-api-client
working_dir: /code/libs/git-service-api-client

dev-php-api-client-base:
<<: *dev82
image: keboola/php-api-client-base
working_dir: /code/libs/php-api-client-base

dev-vault-api-client:
<<: *dev82
image: keboola/vault-api-client
Expand Down
3 changes: 3 additions & 0 deletions libs/php-api-client-base/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/vendor/
/composer.lock
/.phpunit.result.cache
119 changes: 119 additions & 0 deletions libs/php-api-client-base/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Keboola PHP API Client Base

Shared base for building PHP clients for **Keboola services** (Storage, Manage,
Vault, Git Service, Sandboxes, Sync Actions, Job Queue, …). It is **not** a
general-purpose HTTP client — it encodes Keboola platform conventions: the
Keboola authentication headers, retry behavior, JSON handling, and error
normalization that every Keboola service client needs.

Used by `keboola/vault-api-client`, `keboola/sandboxes-service-api-client`,
`keboola/git-service-api-client`, `keboola/sync-actions-client`,
`keboola/azure-api-client`, and new Keboola service clients.

## Installation

```bash
composer require keboola/php-api-client-base
```

## What it provides

- `ApiClient` — Guzzle wrapper with per-request auth, retry, logging, and
response-to-model mapping. Constructed as
`new ApiClient($baseUrl, $authenticator, $options, errorMessageResolver: ..., retryableStatusCodes: [...])`.
The authenticator is **required**; pass `new NoAuthAuthenticator()` for
unauthenticated clients. `errorMessageResolver` accepts a
`?ErrorMessageResolverInterface` instance; when `null`, the shipped
`DefaultErrorMessageResolver` (which extracts `error` or `message` from JSON
bodies) is used automatically. `retryableStatusCodes` are `ApiClient`
constructor arguments supplied by the service facade (they describe the
service's API contract, not caller preferences).
- `ApiClientOptions` — retries, timeouts, logger (no auth, no error resolver — the
authenticator is a first-class `ApiClient` constructor argument; the error
resolver and retryable codes are also `ApiClient` constructor arguments).
- `Auth\RequestAuthenticatorInterface` + ready authenticators for the Keboola
auth schemes: `StorageApiTokenAuthenticator` (`X-StorageApi-Token`),
`ManageApiTokenAuthenticator` (`X-KBC-ManageApiToken`),
`KeboolaServiceAccountAuthenticator` (projected SA token →
`X-Kubernetes-Authorization`), `NoAuthAuthenticator` (explicit no-op for
unauthenticated calls).
- `ErrorMessageResolverInterface`, `DefaultErrorMessageResolver`, `RetryDecider`,
`Json`, `ResponseModelInterface`, `Exception\ClientException`.

## Building a Keboola service client

Compose an `ApiClient` inside your service facade and map responses to models:

```php
use GuzzleHttp\Psr7\Request;
use Keboola\ApiClientBase\ApiClient;
use Keboola\ApiClientBase\ApiClientOptions;
use Keboola\ApiClientBase\Auth\StorageApiTokenAuthenticator;
use Keboola\ApiClientBase\ErrorMessageResolverInterface;
use Keboola\ApiClientBase\Json;
use Keboola\ApiClientBase\ResponseModelInterface;

final class WidgetModel implements ResponseModelInterface
{
public function __construct(public readonly string $id) {}

public static function fromResponseData(array $data): static
{
\assert(is_string($data['id']));
return new self($data['id']);
}
}

final class MyServiceErrorResolver implements ErrorMessageResolverInterface
{
public function __invoke(string $responseBody, int $statusCode): ?string
{
/** @var array{error?: string} $data */
$data = json_decode($responseBody, true) ?? [];
return isset($data['error']) && $data['error'] !== '' ? $data['error'] : null;
}
}

final class MyServiceClient
{
private ApiClient $apiClient;

public function __construct(
string $baseUrl,
StorageApiTokenAuthenticator $authenticator,
?ApiClientOptions $options = null,
) {
$this->apiClient = new ApiClient(
$baseUrl,
$authenticator,
$options,
errorMessageResolver: new MyServiceErrorResolver(),
retryableStatusCodes: [429],
);
}

public function createWidget(string $name): WidgetModel
{
return $this->apiClient->sendRequestAndMapResponse(
new Request('POST', 'widgets', ['Content-Type' => 'application/json'], Json::encodeArray(['name' => $name])),
WidgetModel::class,
);
}
}

$client = new MyServiceClient(
'https://my-service.keboola.com',
new StorageApiTokenAuthenticator($storageApiToken),
new ApiClientOptions(backoffMaxTries: 3),
);
```

## Authentication

Pick the authenticator matching the service's scheme, or implement
`RequestAuthenticatorInterface` for a service-specific scheme (e.g. azure's
OAuth).

## License

MIT
6 changes: 6 additions & 0 deletions libs/php-api-client-base/azure-pipelines.tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
jobs:
- template: ../../azure-pipelines/jobs/run-tests.yml
parameters:
displayName: Tests
serviceName: dev-php-api-client-base
testCommand: bash -c 'composer install && composer ci'
56 changes: 56 additions & 0 deletions libs/php-api-client-base/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "keboola/php-api-client-base",
"type": "library",
"license": "MIT",
"description": "Shared base for Keboola service API clients (transport, auth, retry)",
"authors": [
{
"name": "Keboola",
"email": "devel@keboola.com"
}
],
"autoload": {
"psr-4": {
"Keboola\\ApiClientBase\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Keboola\\ApiClientBase\\Tests\\": "tests/"
}
},
"require": {
"php": "^8.2",
"guzzlehttp/guzzle": "^7.8",
"psr/http-message": "^1.0|^2.0",
"psr/log": "^1.0|^2.0|^3.0",
"webmozart/assert": "^1.11"
},
"require-dev": {
"keboola/coding-standard": "^15.0",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-phpunit": "^1.3",
"phpstan/phpstan-webmozart-assert": "^1.2",
"phpunit/phpunit": "^9.6",
"sempro/phpunit-pretty-print": "^1.4"
},
"config": {
"lock": false,
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"scripts": {
"ci": [
"@composer validate --no-check-publish --no-check-all",
"@phpcs",
"@phpstan",
"@phpunit"
],
"phpcs": "phpcs -n --ignore=vendor,cache --extensions=php .",
"phpcbf": "phpcbf --extensions=php src tests",
"phpstan": "phpstan analyse --no-progress -c phpstan.neon",
"phpunit": "phpunit"
}
}
8 changes: 8 additions & 0 deletions libs/php-api-client-base/phpcs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<ruleset name="Project">
<rule ref="vendor/keboola/coding-standard/src/ruleset.xml">
<exclude name="SlevomatCodingStandard.TypeHints.ParameterTypeHint"/>
<exclude name="SlevomatCodingStandard.TypeHints.PropertyTypeHint"/>
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHint"/>
</rule>
</ruleset>
10 changes: 10 additions & 0 deletions libs/php-api-client-base/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
parameters:
checkMissingIterableValueType: false
level: max
paths:
- src
- tests

includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-webmozart-assert/extension.neon
17 changes: 17 additions & 0 deletions libs/php-api-client-base/phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
cacheResultFile="/tmp/.phpunit.result.cache">
<testsuites>
<testsuite name="php-api-client-base">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>
Loading