PHP ile REST API Entegrasyonu: Kapsamlı Bir Rehber

Modern web uygulamalarında API entegrasyonu artık bir lüks değil, zorunluluk haline geldi. E-ticaret sistemlerinden ödeme altyapılarına, hava durumu servislerinden sosyal medya platformlarına kadar neredeyse her şey bir API üzerinden konuşuyor. PHP, bu alanda hâlâ en yaygın kullanılan dillerden biri ve doğru araçlarla REST API entegrasyonu oldukça güçlü bir hale gelebiliyor. Bu yazıda sıfırdan başlayarak production ortamında kullanabileceğiniz, güvenli ve sürdürülebilir bir PHP REST API istemcisi nasıl inşa edilir, bunu inceleyeceğiz.

Temel Kavramlar ve Hazırlık

REST API ile çalışmadan önce birkaç şeyi netleştirmek gerekiyor. REST (Representational State Transfer), HTTP protokolü üzerine kurulu bir mimari yaklaşım. GET, POST, PUT, PATCH, DELETE gibi HTTP metodlarını kullanarak kaynaklarla etkileşime geçiyorsunuz. PHP tarafında bu işlemleri yapmak için iki ana yöntem var:

  • cURL: PHP’nin built-in kütüphanesi, düşük seviyeli kontrol sağlar
  • Guzzle HTTP: Composer üzerinden gelen modern HTTP istemcisi, daha temiz API sunar

Production ortamında Guzzle’ı tercih ediyorum çünkü exception yönetimi, middleware desteği ve async request özellikleri hayat kurtarıyor. Ama cURL’ü de bilmek şart, çünkü bazen shared hosting’lerde ya da minimal Docker image’larında Composer kullanamıyorsunuz.

Önce gerekli araçları kuralım:

# Composer ile Guzzle kurulumu
composer require guzzlehttp/guzzle

# PHP cURL eklentisinin kurulu olup olmadığını kontrol et
php -m | grep curl

# Ubuntu/Debian sistemlerde eksikse
sudo apt-get install php-curl
sudo systemctl restart php8.2-fpm

# CentOS/RHEL sistemlerde
sudo yum install php-curl
sudo systemctl restart php-fpm

cURL ile Temel API İstekleri

cURL, PHP dünyasının İsviçre çakısı gibi bir araç. Her şeyi yapabiliyor ama biraz verbose. Gerçek dünyada karşılaşacağınız senaryolara göre hazırladığım örneklere bakalım:

# Terminal üzerinden önce API'yi test edelim (curl komut satırı)
curl -X GET "https://jsonplaceholder.typicode.com/posts/1" 
  -H "Accept: application/json" 
  -H "Authorization: Bearer your_token_here" 
  -v

Şimdi PHP tarafında aynı işlemi yapalım:

<?php

class CurlApiClient
{
    private string $baseUrl;
    private string $token;
    private int $timeout;

    public function __construct(string $baseUrl, string $token, int $timeout = 30)
    {
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->token = $token;
        $this->timeout = $timeout;
    }

    private function request(string $method, string $endpoint, array $data = []): array
    {
        $url = $this->baseUrl . '/' . ltrim($endpoint, '/');
        $ch = curl_init();

        $headers = [
            'Content-Type: application/json',
            'Accept: application/json',
            'Authorization: Bearer ' . $this->token,
        ];

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->timeout,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_CUSTOMREQUEST => strtoupper($method),
        ]);

        if (!empty($data) && in_array($method, ['POST', 'PUT', 'PATCH'])) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($error) {
            throw new RuntimeException("cURL Error: " . $error);
        }

        $decoded = json_decode($response, true);

        if ($httpCode >= 400) {
            throw new RuntimeException(
                "API Error {$httpCode}: " . ($decoded['message'] ?? 'Unknown error')
            );
        }

        return $decoded ?? [];
    }

    public function get(string $endpoint, array $params = []): array
    {
        if (!empty($params)) {
            $endpoint .= '?' . http_build_query($params);
        }
        return $this->request('GET', $endpoint);
    }

    public function post(string $endpoint, array $data): array
    {
        return $this->request('POST', $endpoint, $data);
    }

    public function put(string $endpoint, array $data): array
    {
        return $this->request('PUT', $endpoint, $data);
    }

    public function delete(string $endpoint): array
    {
        return $this->request('DELETE', $endpoint);
    }
}

// Kullanım örneği
$client = new CurlApiClient(
    'https://api.orneksite.com/v1',
    getenv('API_TOKEN')
);

$kullanici = $client->get('/users/42');
echo "Kullanıcı: " . $kullanici['name'] . PHP_EOL;

Bu sınıfın önemli bir özelliği SSL doğrulamasını kapatmaması. Pratikte CURLOPT_SSL_VERIFYPEER => false şeklinde yazan pek çok örnek görüyorum, bu güvenlik açığı yaratır. Üretim ortamında asla bu şekilde kullanmayın.

Guzzle ile Profesyonel API İstemcisi

Guzzle, PHP dünyasında HTTP istekleri için fiili standart haline geldi. Middleware katmanı, retry mekanizması ve async istekler sayesinde ciddi projelerde tercih sebebi:

<?php

use GuzzleHttpClient;
use GuzzleHttpHandlerStack;
use GuzzleHttpMiddleware;
use GuzzleHttpPsr7Request;
use GuzzleHttpExceptionRequestException;
use GuzzleHttpExceptionConnectException;
use PsrHttpMessageResponseInterface;

class GuzzleApiClient
{
    private Client $client;
    private array $defaultHeaders;

    public function __construct(
        string $baseUrl,
        string $apiKey,
        int $maxRetries = 3
    ) {
        $stack = HandlerStack::create();
        $stack->push($this->retryMiddleware($maxRetries));

        $this->defaultHeaders = [
            'Authorization' => 'Bearer ' . $apiKey,
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'X-Request-ID' => uniqid('req_', true),
        ];

        $this->client = new Client([
            'base_uri' => rtrim($baseUrl, '/') . '/',
            'timeout' => 30,
            'connect_timeout' => 10,
            'handler' => $stack,
            'headers' => $this->defaultHeaders,
        ]);
    }

    private function retryMiddleware(int $maxRetries): callable
    {
        return Middleware::retry(
            function (int $retries, Request $request, ?ResponseInterface $response, ?Exception $exception) use ($maxRetries): bool {
                if ($retries >= $maxRetries) {
                    return false;
                }

                // Rate limit (429) ve sunucu hatalarında yeniden dene
                if ($response && in_array($response->getStatusCode(), [429, 500, 502, 503, 504])) {
                    return true;
                }

                // Bağlantı hatalarında yeniden dene
                if ($exception instanceof ConnectException) {
                    return true;
                }

                return false;
            },
            function (int $retries): int {
                // Exponential backoff: 1s, 2s, 4s...
                return (int) pow(2, $retries) * 1000;
            }
        );
    }

    public function get(string $endpoint, array $query = []): array
    {
        try {
            $response = $this->client->get($endpoint, [
                'query' => $query,
            ]);
            return $this->parseResponse($response);
        } catch (RequestException $e) {
            $this->handleException($e);
        }
    }

    public function post(string $endpoint, array $payload): array
    {
        try {
            $response = $this->client->post($endpoint, [
                'json' => $payload,
            ]);
            return $this->parseResponse($response);
        } catch (RequestException $e) {
            $this->handleException($e);
        }
    }

    private function parseResponse(ResponseInterface $response): array
    {
        $body = (string) $response->getBody();
        $data = json_decode($body, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new RuntimeException('Geçersiz JSON yanıtı: ' . json_last_error_msg());
        }

        return $data ?? [];
    }

    private function handleException(RequestException $e): never
    {
        $statusCode = $e->hasResponse() ? $e->getResponse()->getStatusCode() : 0;
        $message = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        throw new RuntimeException("API Hatası [{$statusCode}]: {$message}", $statusCode, $e);
    }
}

Gerçek Dünya Senaryosu: E-ticaret Ödeme Entegrasyonu

Teorik örnekler yeterince anlatılıyor zaten. Gelin gerçek bir senaryo üzerinden gidelim. Diyelim ki bir e-ticaret platformuna ödeme sistemi entegre ediyorsunuz. Bu tür entegrasyonlarda webhook yönetimi, idempotency key kullanımı ve güvenli imza doğrulama kritik öneme sahip:

<?php

class PaymentGatewayService
{
    private GuzzleApiClient $client;
    private string $webhookSecret;

    public function __construct(string $apiKey, string $webhookSecret, bool $sandbox = false)
    {
        $baseUrl = $sandbox
            ? 'https://sandbox.odeme-sistemi.com/api/v2'
            : 'https://api.odeme-sistemi.com/api/v2';

        $this->client = new GuzzleApiClient($baseUrl, $apiKey);
        $this->webhookSecret = $webhookSecret;
    }

    public function createPaymentIntent(
        int $amount,
        string $currency,
        string $orderId,
        array $customerData
    ): array {
        // Idempotency key ile aynı isteğin çift işlenmesini önlüyoruz
        $idempotencyKey = hash('sha256', $orderId . $amount . $currency);

        return $this->client->post('/payment-intents', [
            'amount' => $amount, // Kuruş cinsinden
            'currency' => strtoupper($currency),
            'order_id' => $orderId,
            'idempotency_key' => $idempotencyKey,
            'customer' => [
                'email' => $customerData['email'],
                'name' => $customerData['name'],
                'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
            ],
            'metadata' => [
                'platform' => 'myshop_v2',
                'created_at' => date('c'),
            ],
        ]);
    }

    public function verifyWebhookSignature(string $payload, string $signature): bool
    {
        $expectedSignature = hash_hmac('sha256', $payload, $this->webhookSecret);
        // Timing attack'a karşı hash_equals kullan, === değil
        return hash_equals($expectedSignature, $signature);
    }

    public function processWebhook(string $rawPayload, string $signature): void
    {
        if (!$this->verifyWebhookSignature($rawPayload, $signature)) {
            http_response_code(401);
            throw new RuntimeException('Geçersiz webhook imzası');
        }

        $event = json_decode($rawPayload, true);

        match ($event['type']) {
            'payment.completed' => $this->handlePaymentCompleted($event['data']),
            'payment.failed' => $this->handlePaymentFailed($event['data']),
            'refund.created' => $this->handleRefundCreated($event['data']),
            default => error_log("Bilinmeyen webhook olayı: " . $event['type']),
        };

        http_response_code(200);
        echo json_encode(['status' => 'received']);
    }

    private function handlePaymentCompleted(array $data): void
    {
        // Veritabanı güncellemesi, e-posta gönderimi vb.
        error_log("Ödeme tamamlandı: " . $data['payment_id']);
    }

    private function handlePaymentFailed(array $data): void
    {
        error_log("Ödeme başarısız: " . $data['payment_id'] . " - " . $data['failure_reason']);
    }

    private function handleRefundCreated(array $data): void
    {
        error_log("İade oluşturuldu: " . $data['refund_id']);
    }
}

Rate Limiting ve Cache Yönetimi

API entegrasyonlarında en çok karşılaşılan sorun rate limiting’dir. Özellikle toplu işlemler yapan uygulamalarda bu kritik bir konu. Şu stratejiyi kullanıyorum:

<?php

class RateLimitedApiClient
{
    private GuzzleApiClient $client;
    private array $cache = [];
    private int $requestCount = 0;
    private float $windowStart;
    private int $maxRequestsPerMinute;

    public function __construct(GuzzleApiClient $client, int $maxRequestsPerMinute = 60)
    {
        $this->client = $client;
        $this->maxRequestsPerMinute = $maxRequestsPerMinute;
        $this->windowStart = microtime(true);
    }

    public function get(string $endpoint, array $params = [], int $cacheTtl = 300): array
    {
        $cacheKey = md5($endpoint . serialize($params));

        // Cache'den dön
        if (isset($this->cache[$cacheKey])) {
            $cached = $this->cache[$cacheKey];
            if (time() - $cached['time'] < $cacheTtl) {
                return $cached['data'];
            }
        }

        $this->throttle();

        $data = $this->client->get($endpoint, $params);

        // Cache'e yaz
        $this->cache[$cacheKey] = [
            'data' => $data,
            'time' => time(),
        ];

        return $data;
    }

    private function throttle(): void
    {
        $elapsed = microtime(true) - $this->windowStart;

        if ($elapsed >= 60) {
            $this->requestCount = 0;
            $this->windowStart = microtime(true);
        }

        if ($this->requestCount >= $this->maxRequestsPerMinute) {
            $sleepTime = 60 - $elapsed;
            if ($sleepTime > 0) {
                error_log("Rate limit doldu, {$sleepTime} saniye bekleniyor...");
                sleep((int) ceil($sleepTime));
                $this->requestCount = 0;
                $this->windowStart = microtime(true);
            }
        }

        $this->requestCount++;
    }

    public function batchGet(array $endpoints): array
    {
        $results = [];
        foreach ($endpoints as $key => $endpoint) {
            $results[$key] = $this->get($endpoint);
            // İstekler arasında küçük bir bekleme süresi
            usleep(100000); // 100ms
        }
        return $results;
    }
}

Güvenlik: API Anahtarı Yönetimi

API anahtarlarını kod içinde saklamak en yaygın ve en tehlikeli hatalardan biri. Bunu çözmek için şu yaklaşımı kullanıyorum:

# .env dosyası oluştur (asla git'e ekleme!)
cat > .env << 'EOF'
API_BASE_URL=https://api.orneksite.com/v1
API_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxx
WEBHOOK_SECRET=whsec_yyyyyyyyyyyyyyyyyyy
PAYMENT_SANDBOX=false
EOF

# .gitignore'a ekle
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore

# Dosya izinlerini kısıtla
chmod 600 .env
<?php
// Basit .env parser - production'da vlucas/phpdotenv kullanın
function loadEnv(string $path): void
{
    if (!file_exists($path)) {
        throw new RuntimeException(".env dosyası bulunamadı: {$path}");
    }

    $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

    foreach ($lines as $line) {
        if (strpos(trim($line), '#') === 0) {
            continue; // Yorum satırı
        }

        if (strpos($line, '=') !== false) {
            [$key, $value] = explode('=', $line, 2);
            $key = trim($key);
            $value = trim($value, " tnrx0B"'");

            if (!array_key_exists($key, $_ENV)) {
                $_ENV[$key] = $value;
                putenv("{$key}={$value}");
            }
        }
    }
}

loadEnv(__DIR__ . '/.env');

// Kullanım
$apiKey = getenv('API_KEY') ?: throw new RuntimeException('API_KEY tanımlı değil');
$client = new GuzzleApiClient(
    getenv('API_BASE_URL'),
    $apiKey
);

Hata Yönetimi ve Loglama

Production ortamında hata yönetimi olmadan API entegrasyonu yapmak yangın söndürücüsüz bir mutfakta çalışmak gibi. Monolog ile yapılandırılmış loglama:

# Monolog kurulumu
composer require monolog/monolog
<?php

use MonologLogger;
use MonologHandlerRotatingFileHandler;
use MonologHandlerSlackWebhookHandler;
use MonologFormatterJsonFormatter;

class ApiLogger
{
    private Logger $logger;

    public function __construct(string $serviceName)
    {
        $this->logger = new Logger($serviceName);

        // Günlük dönen log dosyaları (30 gün sakla)
        $fileHandler = new RotatingFileHandler(
            '/var/log/myapp/api.log',
            30,
            Logger::DEBUG
        );
        $fileHandler->setFormatter(new JsonFormatter());
        $this->logger->pushHandler($fileHandler);

        // Kritik hatalar için Slack bildirimi
        if (getenv('APP_ENV') === 'production') {
            $slackHandler = new SlackWebhookHandler(
                getenv('SLACK_WEBHOOK_URL'),
                '#api-alerts',
                'API Monitor',
                true,
                null,
                false,
                false,
                Logger::CRITICAL
            );
            $this->logger->pushHandler($slackHandler);
        }
    }

    public function logRequest(string $method, string $url, array $payload = []): void
    {
        $this->logger->info('API İsteği', [
            'method' => $method,
            'url' => $url,
            'payload_size' => strlen(json_encode($payload)),
            'timestamp' => microtime(true),
        ]);
    }

    public function logResponse(int $statusCode, float $responseTime, string $endpoint): void
    {
        $level = $statusCode >= 500 ? Logger::ERROR :
                ($statusCode >= 400 ? Logger::WARNING : Logger::INFO);

        $this->logger->log($level, 'API Yanıtı', [
            'status_code' => $statusCode,
            'response_time_ms' => round($responseTime * 1000, 2),
            'endpoint' => $endpoint,
        ]);
    }

    public function logError(string $message, Throwable $exception): void
    {
        $this->logger->critical($message, [
            'exception' => $exception->getMessage(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
            'trace' => $exception->getTraceAsString(),
        ]);
    }
}

Test Ortamı Hazırlama

API entegrasyonlarını test etmek için mock server kullanmak hayat kurtarıyor. Hem gerçek API’ye bağımlılığı azaltıyor hem de maliyeti düşürüyor:

# WireMock ile mock API server kurulumu
docker run -d 
  --name wiremock 
  -p 8080:8080 
  wiremock/wiremock:latest 
  --verbose

# Mock endpoint tanımla
curl -X POST http://localhost:8080/__admin/mappings 
  -H "Content-Type: application/json" 
  -d '{
    "request": {
      "method": "GET",
      "url": "/api/v1/users/1"
    },
    "response": {
      "status": 200,
      "headers": {
        "Content-Type": "application/json"
      },
      "body": "{"id": 1, "name": "Test Kullanicisi", "email": "[email protected]"}"
    }
  }'

# Testi çalıştır
curl http://localhost:8080/api/v1/users/1
<?php
// PHPUnit ile API client testi
use PHPUnitFrameworkTestCase;
use GuzzleHttpClient;
use GuzzleHttpHandlerMockHandler;
use GuzzleHttpHandlerStack;
use GuzzleHttpPsr7Response;

class PaymentServiceTest extends TestCase
{
    public function testCreatePaymentIntentSuccess(): void
    {
        $mockPayload = json_encode([
            'payment_id' => 'pay_test_123',
            'status' => 'pending',
            'amount' => 15000,
            'currency' => 'TRY',
        ]);

        $mock = new MockHandler([
            new Response(201, ['Content-Type' => 'application/json'], $mockPayload),
        ]);

        $handlerStack = HandlerStack::create($mock);
        $guzzle = new Client(['handler' => $handlerStack]);

        // Test client'ı inject et
        $result = $guzzle->post('/payment-intents', ['json' => [
            'amount' => 15000,
            'currency' => 'TRY',
        ]]);

        $data = json_decode((string) $result->getBody(), true);

        $this->assertEquals('pay_test_123', $data['payment_id']);
        $this->assertEquals('pending', $data['status']);
        $this->assertEquals(201, $result->getStatusCode());
    }
}

Dikkat Edilmesi Gereken Noktalar

Yıllar içinde API entegrasyonlarında öğrendiğim bazı önemli dersler var:

  • Timeout değerlerini her zaman belirtin: Default timeout sonsuz olabilir, bu uzun süre takılı kalan işlemlere yol açar
  • Response’u her zaman doğrulayın: Karşı taraf her zaman söylediği formatta cevap vermez, beklenmedik formatlar için hazırlıklı olun
  • Hassas verileri loglamayın: Kredi kartı numarası, şifre, API key gibi veriler log dosyalarına kesinlikle yazılmamalı
  • Pagination’ı doğru yönetin: Büyük veri setlerinde sayfalama yapmadan tüm veriyi çekmeye kalkmak hem API’yi hem de uygulamanızı çökertir
  • Circuit breaker pattern kullanın: Bir servis sürekli hata veriyorsa istekleri geçici olarak kesmek sistemin geri kalanını korur
  • API versiyonlamasını takip edin: Kullandığınız API’nin deprecation duyurularını mutlaka takip edin, beklenmedik bir gün tüm entegrasyon çalışmayabilir
  • Connection pooling’e dikkat edin: PHP’de her request yeni bir process açar, persistent bağlantılar için Redis veya benzeri bir ara katman kullanın

Sonuç

PHP ile REST API entegrasyonu, doğru araçları ve doğru yaklaşımları kullandığınızda hem güvenli hem de bakımı kolay bir yapıya dönüşüyor. cURL ile temel işlemleri anlayarak başlayın, production projelerinizde Guzzle’a geçin. Rate limiting, cache, retry mekanizması ve güvenli loglama olmadan bir API entegrasyonu yarım kalmış sayılır.

Webhook güvenliğini asla ihmal etmeyin, API anahtarlarınızı environment variable’larda saklayın ve her şeyi test edin. Bir API entegrasyonunun ne zaman bozulacağını bilemezsiniz, ama hazırlıklı olduğunuzda etkisini minimize edebilirsiniz. Sonuç olarak iyi bir API istemcisi yazmak, sadece istek göndermek değil; hataları zarif biçimde yönetmek, sistemi izlemek ve uzun vadede sürdürülebilir tutmak demek.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir