When building integrations with multiple external services, structuring them as Laravel service providers creates a clean, reusable plugin architecture. Here’s how to build HTTP clients with automatic request/response logging using service providers.
The Pattern
Each integration is a self-contained “plugin” with its own service provider that:
- Registers the HTTP client with middleware
- Wires dependencies via DI
- Configures logging/monitoring
- Implements capability interfaces
Structure
app/
โโโ Integrations/
โ โโโ ShipmentTracking/
โ โโโ ServiceProvider.php # DI + middleware wiring
โ โโโ Client.php # High-level client
โ โโโ ShipmentTrackingPlugin.php # Implements capability interfaces
โ โโโ SDK/
โ โโโ ApiClient.php # Low-level HTTP client
โ โโโ Model/ # Response DTOs
Implementation
Step 1: Service Provider with HTTP Logging
// app/Integrations/ShipmentTracking/ServiceProvider.php
namespace App\Integrations\ShipmentTracking;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class ServiceProvider extends BaseServiceProvider
{
public function register()
{
$this->app->singleton(SDK\ApiClient::class, function ($app) {
$stack = HandlerStack::create();
// Add request/response logging middleware
$stack->push($this->loggingMiddleware());
return new SDK\ApiClient([
'base_uri' => config('services.shipment_tracking.base_url'),
'handler' => $stack,
'timeout' => 30,
]);
});
$this->app->singleton(Client::class, function ($app) {
return new Client(
$app->make(SDK\ApiClient::class)
);
});
}
protected function loggingMiddleware(): callable
{
return Middleware::tap(
function (RequestInterface $request) {
\Log::info('[ShipmentTracking] Request', [
'method' => $request->getMethod(),
'uri' => (string) $request->getUri(),
'headers' => $request->getHeaders(),
'body' => (string) $request->getBody(),
]);
},
function (RequestInterface $request, $options, ResponseInterface $response) {
\Log::info('[ShipmentTracking] Response', [
'status' => $response->getStatusCode(),
'body' => (string) $response->getBody(),
'duration_ms' => $options['duration'] ?? null,
]);
}
);
}
}
Step 2: High-Level Client
// app/Integrations/ShipmentTracking/Client.php
namespace App\Integrations\ShipmentTracking;
use App\Integrations\ShipmentTracking\SDK\ApiClient;
class Client
{
public function __construct(
private readonly ApiClient $apiClient
) {}
public function getShipmentStatus(string $trackingNumber): ShipmentStatus
{
$response = $this->apiClient->get("/tracking/{$trackingNumber}");
return $this->serializer->deserialize(
$response->getBody(),
ShipmentStatus::class,
'json'
);
}
public function listShipments(array $filters = []): array
{
$response = $this->apiClient->get('/shipments', [
'query' => $filters,
]);
return $this->serializer->deserialize(
$response->getBody(),
'array',
'json'
);
}
}
Step 3: Plugin with Multiple Capabilities
// app/Integrations/ShipmentTracking/ShipmentTrackingPlugin.php
namespace App\Integrations\ShipmentTracking;
use App\Contracts\TracksShipments;
use App\Contracts\ImportsInventory;
class ShipmentTrackingPlugin implements TracksShipments, ImportsInventory
{
public function __construct(
private readonly Client $client
) {}
public function trackShipment(string $trackingNumber): array
{
$status = $this->client->getShipmentStatus($trackingNumber);
return [
'status' => $status->currentStatus,
'location' => $status->currentLocation,
'estimated_delivery' => $status->estimatedDeliveryDate,
'history' => $status->events,
];
}
public function syncInventory(string $warehouseId): void
{
$shipments = $this->client->listShipments([
'warehouse' => $warehouseId,
'status' => 'in_transit',
]);
foreach ($shipments as $shipment) {
// Update local inventory records
Inventory::updateOrCreate(
['tracking_number' => $shipment->trackingNumber],
['quantity' => $shipment->quantity, 'eta' => $shipment->eta]
);
}
}
}
Step 4: Register the Provider
// config/app.php
'providers' => ServiceProvider::defaultProviders()->merge([
// ...
App\Integrations\ShipmentTracking\ServiceProvider::class,
])->toArray(),
Benefits
1. Automatic HTTP Logging
Every API call is logged without manual instrumentation. Perfect for debugging integration issues:
[2026-03-19 14:30:12] [ShipmentTracking] Request
method: GET
uri: https://api.shipmenttracker.com/tracking/ABC123
duration: 342ms
[2026-03-19 14:30:12] [ShipmentTracking] Response
status: 200
body: {"status":"delivered","location":"Singapore"}
2. Testability
Mock the high-level Client in tests, not Guzzle:
$mockClient = Mockery::mock(Client::class);
$mockClient->shouldReceive('getShipmentStatus')
->with('ABC123')
->andReturn(new ShipmentStatus(['status' => 'delivered']));
$this->app->instance(Client::class, $mockClient);
3. Reusable Pattern
Copy this structure for every new integration:
- Payment gateways
- Shipping providers
- CRM systems
- Marketing automation
4. Interface-Based Architecture
Plugins implement capability interfaces (TracksShipments, ImportsInventory), allowing multiple providers for the same capability:
// Swap providers without changing consuming code
interface TracksShipments
{
public function trackShipment(string $trackingNumber): array;
}
// Use any provider that implements the interface
$tracker = app(TracksShipments::class); // Could be FedEx, DHL, UPS, etc.
$status = $tracker->trackShipment('ABC123');
Advanced: Environment-Specific Middleware
Add different middleware based on environment:
protected function loggingMiddleware(): callable
{
if (app()->environment('production')) {
// Production: log only errors and slow requests
return Middleware::tap(
null,
function ($request, $options, $response) {
if ($response->getStatusCode() >= 400 || ($options['duration'] ?? 0) > 5000) {
\Log::warning('[ShipmentTracking] Slow/Error', [
'status' => $response->getStatusCode(),
'duration_ms' => $options['duration'],
'uri' => (string) $request->getUri(),
]);
}
}
);
}
// Development/Staging: log everything
return $this->verboseLoggingMiddleware();
}
This pattern scales to dozens of integrations while keeping each one isolated, testable, and easy to maintain.