Üçüncü parti API tüketmek: Guzzle ile HTTP istemcisi
PHP'de Guzzle HTTP istemcisi ile dış servislere bağlanmak; timeout, hata yönetimi ve güvenilir istek yapısı kurmak.
Bir uygulama büyüdükçe başka servislere bağlanmak kaçınılmaz oluyor. Ödeme sağlayıcısı, SMS servisi, hava durumu API’si, kargo takip servisi… Bu entegrasyonların her biri kendi curl_init() yığınıyla yapılırsa, zamanla her birinin nasıl davrandığını anlamak güçleşiyor.
Guzzle, PHP için geliştirilmiş HTTP client kütüphanesi. PSR-7 uyumlu mesajları destekliyor, senkron ve asenkron istekler gönderilebiliyor, hata yönetimi tutarlı bir yapıya oturmuş. Laravel ve Symfony gibi framework’ler bu kütüphaneye zaten bağımlı; büyük ihtimalle projenizde zaten kurulu.
Kurulum
Henüz kurulu değilse Composer ile eklemek yeterli:
composer require guzzlehttp/guzzle
Basit bir GET isteği
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://api.example.com',
]);
$response = $client->get('/users/42');
$data = json_decode($response->getBody(), true);
base_uri parametresi sayesinde her istekte tam URL’i tekrar yazmak gerekmiyor. Bütün isteklerin aynı temel adrese gitmesi gereken entegrasyonlarda bu küçük şeyi seviyorum.
Timeout ayarlamak
Dış servisler her zaman hızlı cevap vermez. Timeout belirlemeden istek yapmak, sunucunuzun o servisi cevap gelene kadar sonsuza kadar beklemesi anlamına gelebilir. Kritik bir akışta bu kabul edilemez.
$client = new Client([
'base_uri' => 'https://api.example.com',
'timeout' => 5.0, // 5 saniyede yanıt gelmezse exception fırlatır
'connect_timeout' => 2.0, // Bağlantı kurulumu için maksimum süre
]);
İki farklı timeout var: connect_timeout sunucuya bağlanmanın ne kadar süreceğini, timeout ise bağlantı kurulduktan sonra yanıtın ne kadar bekleneceğini sınırlıyor. Her ikisini de belirlemek iyi bir alışkanlık.
Timeout değerlerini belirlerken “en kötü normal senaryo”yu düşünmek gerekiyor. Bir ödeme sağlayıcısı normal koşullarda 800 ms yanıt veriyorsa, 5 saniyelik timeout makul. Ama 30 saniye koyarsanız, o servis yavaşladığında kullanıcı yarım dakika boyunca ekran karşısında bekler. Bunu kargo entegrasyonunda ağır biçimde yaşadım: varsayılan timeout yoktu, servis bir gün yavaşlayınca tüm sipariş sayfaları kilitlendi.
Hata yönetimi
Guzzle, 4xx ve 5xx yanıtlarında varsayılan olarak exception fırlatır. Bunu try/catch ile yakalamak gerekiyor:
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\RequestException;
try {
$response = $client->post('/orders', [
'json' => ['product_id' => 5, 'quantity' => 2],
]);
} catch (ClientException $e) {
// 4xx: istemci hatası (geçersiz istek, yetkisiz vb.)
$statusCode = $e->getResponse()->getStatusCode();
// Loglama veya kullanıcıya uygun mesaj
} catch (ServerException $e) {
// 5xx: dış servis tarafında hata
// Yeniden deneme mantığı burada değerlendirilebilir
} catch (RequestException $e) {
// Bağlantı hatası, timeout vb. ağ seviyesi sorunlar
}
Her exception türü farklı anlama geliyor; bunları ayırt etmek önemli. 4xx hatalar genellikle uygulamanızın gönderdiği verinin yanlış olduğuna işaret eder; 5xx hatalar dış servisin sorununu gösterir. İkisine aynı şekilde davranmak yanıltıcı olabilir.
Bir tuzak daha var: bazı API’ler hataları HTTP durum kodu yerine her zaman 200 döndürüp response body’e yazıyor. Bu durumda Guzzle exception fırlatmaz; yanıtı parse edip kendiniz kontrol etmek zorunda kalırsınız. Entegrasyona başlamadan önce servisin hata davranışını dokümantasyondan okuyun; yoksa ürün ortamında 200 gelen ama içinde {"success": false} yazan bir yanıtı sessizce işlemiş olursunuz.
Header ve kimlik doğrulama
Çoğu API kimlik doğrulaması için bir token ya da API anahtarı istiyor:
$client = new Client([
'base_uri' => 'https://api.example.com',
'headers' => [
'Authorization' => 'Bearer ' . config('services.example.token'),
'Accept' => 'application/json',
],
]);
Token gibi hassas değerleri kodun içine yazmak yerine .env dosyasından çekmek önemli. Kaynak kodda açık metin token bırakmak kolay yapılan, geç fark edilen bir hata.
Servis sınıfına sarmak
Doğrudan Guzzle istemcisini her yerde new Client() ile oluşturmak yerine, entegrasyon için bir servis sınıfı yazmak işleri düzenli tutuyor:
class ExampleApiService
{
private Client $client;
public function __construct()
{
$this->client = new Client([
'base_uri' => config('services.example.base_uri'),
'timeout' => 5.0,
'headers' => [
'Authorization' => 'Bearer ' . config('services.example.token'),
],
]);
}
public function getUser(int $id): array
{
$response = $this->client->get("/users/{$id}");
return json_decode($response->getBody(), true);
}
}
Bu yapıyla, Guzzle’ı bir gün başka bir HTTP kütüphanesiyle değiştirseniz ya da mock istemci kullanmak istesseniz, değişiklik tek sınıfla sınırlı kalıyor.
Yanıtı doğrulamak
Bir API belirli bir formatta yanıt döndüreceğini söylüyorsa, bu formatı körü körüne güvenmek yerine kontrol etmek iyi bir alışkanlık. Beklediğiniz anahtar yoksa erken hata vermek, saatler sonra oluşacak beklenmedik bir null hatasından iyidir:
public function getUser(int $id): array
{
$response = $this->client->get("/users/{$id}");
$data = json_decode($response->getBody(), true);
if (!isset($data['id'], $data['email'])) {
throw new \UnexpectedValueException(
"API beklenmedik format döndürdü: " . $response->getBody()
);
}
return $data;
}
Dış servislere bağlanırken en sık gözden kaçan şey timeout değerleri. Her entegrasyon baştan makul bir timeout ve hata yönetimiyle kurulursa, o servis gelecekte yavaşladığında veya erişilemez olduğunda uygulamanızın geri kalanı bundan etkilenmiyor. Kargo entegrasyonumda yaşadığım donma olayından sonra, yeni her entegrasyon için servis sınıfı şablonuma timeout ve yanıt doğrulamasını varsayılan olarak ekledim.