Laravel Service Provider for Plugin Architecture with HTTP Logging

πŸ“– 4 minutes read

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.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *