silversoft / api-client
Klient wewnętrznego API: JSON po HTTP z podpisem RFC 9421, bez zależności zewnętrznych.
Requires
- php: >=7.4
- ext-curl: *
- ext-json: *
- silversoft/api-signer: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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
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.