Sekarang bayangkan aplikasi kita memakai tiga layanan eksternal: cuaca, pembayaran, dan pengiriman. Bila panggilan Http:: tercecer langsung di controller, base URL tersebar di sepuluh tempat, API key berserakan, dan satu perubahan di sisi vendor menjadi perburuan di seluruh codebase. Solusinya adalah pola yang sudah klasik di dunia Laravel: bungkus setiap layanan dalam satu service class — pintu tunggal yang menyembunyikan detail HTTP, menerjemahkan error vendor menjadi exception domain, dan mudah diuji. Chapter ini membangun CuacaService utuh dari nol sampai teruji.
Konfigurasi via services.php #
Mulai dari rumah bagi kredensial. File config/services.php sudah menyediakan tempatnya — kita tambahkan entri untuk layanan cuaca:
<?php
// config/services.php (potongan)
'weather' => [
'base_url' => env('WEATHER_API_URL', 'https://api.example.com'),
'key' => env('WEATHER_API_KEY'),
],Dengan ini kode hanya pernah menyebut config('services.weather.*'); nilai asli tinggal di .env dan tidak pernah ter-commit:
WEATHER_API_URL=https://api.example.com
WEATHER_API_KEY=YOUR_API_KEYDTO untuk Response Terketik #
Response mentah berupa array longgar — ['city' => ...] tanpa jaminan bentuk. Sebelum membangun service, kita definisikan data transfer object (DTO) yang mengikat bentuk payload ke property ber-tipe. Sejak PHP 8.2, readonly class membuat DTO benar-benar tak berubah setelah dibuat:
<?php
// app/Services/Cuaca/Cuaca.php
namespace App\Services\Cuaca;
final readonly class Cuaca
{
public function __construct(
public string $kota,
public float $suhu,
public string $kondisi,
public string $diperbarui,
) {}
public static function fromPayload(array $payload): self
{
return new self(
kota: $payload['city'],
suhu: (float) $payload['temperature'],
kondisi: $payload['condition'],
diperbarui: $payload['updated_at'],
);
}
}Method fromPayload() bertugas menerjemahkan kunci payload vendor (city, temperature) ke istilah aplikasi kita (kota, suhu). Bila suatu hari vendor mengganti nama kunci, hanya satu method inilah yang perlu disesuaikan.
Service Class dengan Client Terinjeksi #
Inilah pusatnya. Perhatikan constructor-nya menerima HttpFactory — kelas di balik facade Http — lewat dependency injection container, bukan memanggil facade langsung:
<?php
// app/Services/Cuaca/CuacaService.php
namespace App\Services\Cuaca;
use App\Exceptions\CuacaException;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Factory as HttpFactory;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Log;
class CuacaService
{
public function __construct(private HttpFactory $http)
{
}
public function cuacaTerkini(string $kota): Cuaca
{
try {
$response = $this->http->baseUrl(config('services.weather.base_url'))
->withToken(config('services.weather.key'))
->timeout(5)
->retry(2, 100)
->get('/current', ['kota' => $kota])
->throw();
} catch (ConnectionException|RequestException $e) {
Log::warning('Panggilan API cuaca gagal', [
'kota' => $kota,
'error' => $e->getMessage(),
]);
throw new CuacaException("Cuaca untuk '{$kota}' tidak dapat diambil.", 0, $e);
}
return Cuaca::fromPayload($response->json());
}
}Satu method publik ini memuat seluruh disiplin integrasi. Rantai request memakai semua senjata chapter-chapter sebelumnya sekaligus: baseUrl() dari config, token dari config, timeout(5), retry(2, 100) untuk kegagalan sesaat, dan throw() agar response gagal tidak lolos diam-diam. Blok catch melakukan dua hal berurutan: mencatat kegagalan ke log, lalu melempar CuacaException — exception domain milik aplikasi — dengan exception asli diselipkan sebagai $previous agar jejak lengkapnya tidak hilang.
Exception domain-nya sendiri sederhana:
<?php
// app/Exceptions/CuacaException.php
namespace App\Exceptions;
use RuntimeException;
class CuacaException extends RuntimeException
{
}Pemanggil service tidak perlu tahu bahwa di baliknya ada cURL, Guzzle, atau status 500 — mereka hanya mengenal satu kemungkinan kegagalan: CuacaException.
Service dalam Aksi #
Karena CuacaService menerima HttpFactory dari container, facade dan service memakai objek yang sama — artinya Http::fake() otomatis berlaku juga untuk request yang dikirim service. Kita buktikan dengan dua skenario, sukses dan gagal:
<?php
use App\Exceptions\CuacaException;
use App\Services\Cuaca\CuacaService;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
$logTertangkap = [];
Log::listen(fn ($event) => $logTertangkap[] = '['.$event->level.'] '.$event->message);
$apiDown = false;
Http::fake([
'api.example.com/current*' => function () use (&$apiDown) {
if ($apiDown) {
return Http::response(['pesan' => 'layanan sedang gangguan'], 500);
}
return Http::response([
'city' => 'Bandung',
'temperature' => 24,
'condition' => 'Berawan',
'updated_at' => '2026-10-13 08:00',
], 200);
},
]);
// Skenario sukses
$cuaca = app(CuacaService::class)->cuacaTerkini('Bandung');
echo get_class($cuaca)."\n";
echo $cuaca->kota.': '.$cuaca->suhu."°C, ".$cuaca->kondisi."\n";
// Skenario gagal
$apiDown = true;
try {
app(CuacaService::class)->cuacaTerkini('Bandung');
} catch (CuacaException $e) {
echo 'terlempar: '.get_class($e)."\n";
echo 'pesan: '.$e->getMessage()."\n";
echo 'penyebab: '.get_class($e->getPrevious())."\n";
}
echo "\n=== log yang tertulis ===\n";
foreach ($logTertangkap as $baris) {
echo $baris."\n";
}Output yang Dihasilkan:
App\Services\Cuaca\Cuaca
Bandung: 24°C, Berawan
terlempar: App\Exceptions\CuacaException
pesan: Cuaca untuk 'Bandung' tidak dapat diambil.
penyebab: Illuminate\Http\Client\RequestException
=== log yang tertulis ===
[warning] Panggilan API cuaca gagalSkenario sukses mengembalikan DTO yang siap dipakai — type-safe dan bebas kunci vendor. Skenario gagal menunjukkan rantai pertahanan bekerja: throw() melempar RequestException, catch mencatatnya ke log, dan pemanggil hanya menerima CuacaException yang rapi.
Integrasi kini punya arsitektur. Satu persoalan terakhir menyisak: bagaimana menguji kode yang memanggil layanan eksternal ini secara rutin tanpa benar-benar menyentuh jaringan? Jawabannya adalah Http::fake() dan kawan-kawannya — topik penutup section ini di chapter Mocking HTTP Requests
.