Search by

silversoft / api-client

rbrzezinski

Klient wewnętrznego API: JSON po HTTP z podpisem RFC 9421, bez zależności zewnętrznych.

Package info

github.com/silversoft-pl/api-client-php

pkg:composer/silversoft/api-client

Statistics

Installs: 12

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-09-20 09:02 UTC

This package is auto-updated.

Last update: 2026-09-20 09:02:46 UTC


README

Klient wewnętrznego API: JSON po HTTP, z podpisem zgodnym z RFC 9421. Bez zależności zewnętrznych — transport to ext-curl, a jedyną zależnością jest nasz silversoft/api-signer.

composer require silversoft/api-client

PHP

Odpowiednik dla Node: @silversoft/api-client.

Do czego, a do czego nie

Do wewnętrznego API — usług, które sami utrzymujemy i które uwierzytelniają się nagłówkiem Api-Authorization albo podpisem RFC 9421. Klient zdejmuje z aplikacji trzy rzeczy, które dziś robi każda z nich osobno: budowanie nagłówka autoryzacji, serializację ciała i interpretację odpowiedzi.

Nie do API zewnętrznych. Usługi trzecich stron zostają na tym, czego używasz do nich dziś. Mają inne uwierzytelnianie, inne formaty i inne wymagania — ciasteczka, przekierowania, formularze, przesyłanie plików — a wciągnięcie tego tutaj zamieniłoby wąskie narzędzie w ogólny klient HTTP, czyli inny produkt.

Szybki start

use Silversoft\ApiClient\Client;

$client = new Client([
    'url'    => $config['url'],
    'key_id' => $config['name'],
    'key'    => $config['key'],
    'auth'   => $config['auth'] ?? 'legacy',   // 'signed' po migracji na podpisy
]);

$response = $client->post('/v1/users/update_profile', ['user' => $data]);

if ($response->error !== null) {
    error_log('API: ' . $response->error);
    return;
}

$user = $response->data->user;

GET z parametrami:

$response = $client->get('/v1/items/list', ['types' => ['seo'], 'od' => '2025-01-01']);

Response

Pole Znaczenie
->data zdekodowany JSON jako stdClass — to, co dziś zwraca $curl->post()
->status kod HTTP; 0 przy błędzie transportu
->error powód niepowodzenia wywołania: kod spoza 2xx, błąd transportu albo niepoprawny JSON. null oznacza powodzenie
->raw surowa odpowiedź, do diagnostyki
->headers nagłówki odpowiedzi, klucze małymi literami
->retries ile ponowień było potrzebne

Wynik wywołania a wynik operacji

$response->error mówi o wywołaniu: czy udało się połączyć, czy serwis odpowiedział kodem 2xx i czy odpowiedź jest poprawnym JSON-em. To, czy serwis wykonał żądaną operację, jest w ciele odpowiedzi — wiele API zwraca to jako pole success:

if ($response->error !== null) {
    // nie udało się wywołać
}
if (!$response->data->success) {
    // wywołanie przeszło, operacji nie wykonano
}

$response->value('pole', $domyslna) czyta z ciała niezależnie od tego, czy jest obiektem, czy tablicą (assoc).

Opcje

Wszystkie ustawia się w konstruktorze; część można nadpisać na pojedynczym żądaniu jako trzeci argument get() / post() / request().

Opcja Domyślnie Znaczenie Per żądanie
url adres bazowy usługi (wymagane) nie
key_id nazwa aplikacji, czyli keyid podpisu (wymagane) nie
key sekret (wymagane) nie
auth signed signed (RFC 9421) albo legacy (Api-Authorization) nie
alg hmac-sha256 algorytm podpisu nie
timeout 30 limit całego żądania w sekundach tak
connect_timeout 10 limit nawiązania połączenia tak
verify true weryfikacja certyfikatu TLS tak
user_agent <key_id> (silversoft/api-client-php) nagłówek User-Agent tak
headers [] własne nagłówki dokładane do każdego żądania tak
curl [] własne opcje CURLOPT_*, nadpisują domyślne tak
retries 0 liczba ponowień tak
retry_delay 200 odstęp w ms, narastająco tak
retry_methods ['GET', 'HEAD'] metody, które wolno ponawiać tak
retry_statuses [429, 500, 502, 503, 504] kody wyzwalające ponowienie tak
tag null parametr tag podpisu tak
lifetime 300 ważność podpisu w sekundach tak
assoc false ->data jako tablica zamiast stdClass tak
$client = new Client([
    'url'        => 'https://service.example.com',
    'key_id'     => 'my-service',
    'key'        => getenv('API_SECRET'),
    'timeout'    => 180,
    'user_agent' => 'my-service/2.1',
    'headers'    => ['X-Zrodlo' => 'reporting'],
    'curl'       => [CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4],
    'retries'    => 3,
]);

$response = $client->post('/v1/files/upload', $payload, ['timeout' => 600]);

Własne nagłówki są objęte podpisem — nie da się ich dostrzyknąć ani usunąć po drodze. Opcje curl są stosowane po domyślnych, więc nadpisują to, co ustawia klient.

Ponowienia

Domyślnie wyłączone i po włączeniu obowiązują tylko metody z retry_methods, czyli GET i HEAD. POST nie jest ponawiany, nawet przy retries > 0: bez wiedzy o idempotentności endpointu ponowienie tworzy duplikaty, a /api/add mailera i /v1/files/upload to dokładnie takie przypadki. Jeśli konkretny POST jest idempotentny, włącz to świadomie:

$client->post('/api/idempotentny', $body, ['retries' => 3, 'retry_methods' => ['POST']]);

Duże ładunki

Wewnętrzne API przesyła pliki jako base64 w JSON-ie (tak działa /v1/files/upload), nie jako multipart/form-data — i tak musi zostać, bo podpisane endpointy multipartu nie obsługują. Ciało wielomegabajtowe przechodzi bez problemu; pamiętaj tylko o timeout odpowiednim do rozmiaru.

Migracja ze starego schematu

Tryb autoryzacji jest opcją, nie gałęzią w kodzie:

'auth' => 'legacy'   // Api-Authorization: base64("<key_id>|<klucz>")
'auth' => 'signed'   // RFC 9421 (Signature-Input + Signature)

Dzięki temu aplikacja przechodzi na podpisy zmianą konfiguracji. Typowa migracja miejsca wywołania:

// było
$curl = new Curl();
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, !DEBUG);
$curl->setHeader('Api-Authorization', base64_encode(sprintf('%s|%s', $cfg['name'], $cfg['key'])));
$curl->setHeader('Content-Type', 'application/json');
$result = $curl->post($cfg['url'] . '/v1/session/verify', ['token' => $token]);
if (!$result->success) { /* flaga z ciała odpowiedzi */ }

// jest
$response = $client->post('/v1/session/verify', ['token' => $token]);
if ($response->error !== null) { /* nie udało się wywołać */ }
$result = $response->data;
if (!$result->success) { /* wywołanie przeszło, operacja nie */ }

Zwróć uwagę na dwie rzeczy: $curl->post() przyjmował tablicę i sam ją kodował, a klient serializuje ciało raz i wysyła dokładnie te bajty, które podpisał; oraz dotychczasowe $result->success rozpada się na dwa sprawdzenia — $response->error dla wywołania i $response->data->success dla operacji.

Bezpieczeństwo

Model bezpieczeństwa opisuje api-signer — tu obowiązują te same założenia: TLS przy każdym wywołaniu, jeden klucz na parę usług, jeden klucz na środowisko, co najmniej 32 bajty entropii, klucze nigdy w repozytorium.

Klient nie wyłącza weryfikacji TLS sam z siebie. verify => false bywa potrzebne na lokalnym środowisku z certyfikatem z podpisem własnym, ale poza nim jest błędem.

Testy

Testy idą przez prawdziwy serwer HTTP (php -S) i prawdziwego cURL-a, a serwer testowy weryfikuje podpis tak jak zrobiłaby to aplikacja. Dzięki temu pokryta jest cała droga — podpisanie, transport, weryfikacja — a nie tylko atrapa transportu.

composer install
vendor/bin/phpunit

Do pracy lokalnej composer.json ma repozytorium typu path wskazujące na ../api-signer-php, więc oba repozytoria wystarczy mieć obok siebie.

Licencja

MIT — patrz LICENSE.