update 20.10.2025

This commit is contained in:
Kevin Adametz 2025-10-20 17:42:08 +02:00
parent 8c11130b5d
commit a939cd51ef
616 changed files with 84821 additions and 4121 deletions

View file

@ -1,3 +1,6 @@
#DHL Dokuments
https://developer.dhl.com/api-reference/parcel-de-shipping-post-parcel-germany-v2?language_content_entity=en#get-started-section/
# DHL Laravel Package
A comprehensive Laravel package for DHL shipping, tracking, and returns functionality with direct API integration.
@ -20,7 +23,7 @@ A comprehensive Laravel package for DHL shipping, tracking, and returns function
{
"repositories": [
{
"type": "path",
"type": "path",
"url": "./packages/acme-laravel-dhl"
}
],
@ -83,7 +86,7 @@ $orderData = [
],
'consignee' => [
'name' => 'Customer Name',
'street' => 'Kundenstraße 456', // House number will be auto-parsed
'street' => 'Kundenstraße 456', // House number will be auto-parsed
'postalCode' => '54321',
'city' => 'Munich',
'country' => 'DE'
@ -104,7 +107,7 @@ The package automatically handles German address formats by extracting house num
// Supported formats:
"Musterstraße 123" -> street: "Musterstraße", number: "123"
"Am Markt 7" -> street: "Am Markt", number: "7"
"Am Markt 7" -> street: "Am Markt", number: "7"
"Karl-Marx-Straße 156" -> street: "Karl-Marx-Straße", number: "156"
"Lindenstraße 1-3" -> street: "Lindenstraße", number: "1-3"
"Muster Str. 99" -> street: "Muster Str.", number: "99"
@ -171,7 +174,7 @@ When enabled, label creation and tracking updates will be processed asynchronous
The package includes comprehensive error handling:
- `DhlApiException` - General API errors
- `DhlAuthenticationException` - Authentication failures
- `DhlAuthenticationException` - Authentication failures
- `DhlValidationException` - Data validation errors
## Testing

View file

@ -1,6 +1,7 @@
<?php
return [
'base_url' => env('DHL_BASE_URL', 'https://api-eu.dhl.com'),
'sandbox_url' => env('DHL_SANDBOX_URL', 'https://api-sandbox.dhl.com'),
'api_key' => env('DHL_API_KEY'),
'username' => env('DHL_USERNAME'),
'password' => env('DHL_PASSWORD'),
@ -10,4 +11,15 @@ return [
'profile' => env('DHL_PROFILE', 'STANDARD_GRUPPENPROFIL'),
'webhook' => ['enabled' => env('DHL_WEBHOOK_ENABLED', false), 'secret' => env('DHL_WEBHOOK_SECRET'), 'route' => env('DHL_WEBHOOK_ROUTE', 'dhl/webhooks/tracking')],
'use_queue' => env('DHL_USE_QUEUE', false), // Set to true to enable queue jobs for async processing
'legacy' => [
'test_mode' => env('DHL_TEST_MODE', true),
'sandbox' => env('DHL_SANDBOX', true),
],
'ssl' => [
'verify_peer' => env('DHL_SSL_VERIFY_PEER', true),
'verify_host' => env('DHL_SSL_VERIFY_HOST', true),
'ssl_version' => env('DHL_SSL_VERSION', 'TLSv1_2'),
'timeout' => env('DHL_TIMEOUT', 30),
'connect_timeout' => env('DHL_CONNECT_TIMEOUT', 10),
]
];

View file

@ -17,6 +17,8 @@ return new class extends Migration
$table->string('dhl_shipment_no')->nullable()->index();
$table->enum('type', ['outbound', 'return'])->default('outbound');
$table->unsignedBigInteger('related_shipment_id')->nullable()->index(); // For returns
$table->string('routing_code')->nullable();
// Product and billing
$table->string('product_code')->default('V01PAK');
@ -32,6 +34,12 @@ return new class extends Migration
$table->string('tracking_status')->nullable(); // Last DHL status text
$table->timestamp('last_tracked_at')->nullable();
// Recipient information
$table->string('firstname')->nullable();
$table->string('lastname')->nullable();
$table->string('company')->nullable();
$table->json('recipient')->nullable();
// API response data
$table->json('api_response_data')->nullable();

View file

@ -12,8 +12,12 @@ class DhlServiceProvider extends ServiceProvider
// Bind DhlClient as a singleton
$this->app->singleton(Support\DhlClient::class, function ($app) {
// Check if we're in test/sandbox mode
$isTestMode = config('dhl.legacy.test_mode', false) || config('dhl.legacy.sandbox', false);
$baseUrl = $isTestMode ? config('dhl.sandbox_url') : config('dhl.base_url');
return new Support\DhlClient(
config('dhl.base_url'),
$baseUrl,
config('dhl.api_key'),
config('dhl.username'),
config('dhl.password')

View file

@ -26,12 +26,17 @@ class DhlShipment extends Model
'label_format',
'label_path',
'status',
'firstname',
'lastname',
'company',
'recipient',
'tracking_status',
'last_tracked_at',
'api_response_data'
];
protected $casts = [
'recipient' => 'array',
'api_response_data' => 'array',
'last_tracked_at' => 'datetime',
'weight_kg' => 'decimal:3'
@ -48,6 +53,7 @@ class DhlShipment extends Model
'unknown' => 'unknown'
];
/**
* Get the tracking events for this shipment
*/
@ -146,4 +152,36 @@ class DhlShipment extends Model
{
return $this->status === 'delivered';
}
/**
* Get translated status for current locale
*/
public function getStatusTranslation(): string
{
return __('dhl.status.' . $this->status, [], $this->status);
}
/**
* Get translated status for any status
*/
public static function getStatusTranslationFor(string $status): string
{
return __('dhl.status.' . $status, [], $status);
}
/**
* Get translated type for current locale
*/
public function getTypeTranslation(): string
{
return __('dhl.type.' . $this->type, [], $this->type);
}
/**
* Get translated product code for current locale
*/
public function getProductCodeTranslation(): string
{
return __('dhl.product_codes.' . $this->product_code, [], $this->product_code);
}
}

View file

@ -365,18 +365,29 @@ class ShippingService
*/
private function getBillingNumberForProduct(string $productCode): string
{
// Try to get account number from config by product code
$accountNumber = config("dhl.account_numbers.{$productCode}");
// Check if we're in test/sandbox mode
$isTestMode = config('dhl.legacy.test_mode', false) || config('dhl.legacy.sandbox', false);
if ($accountNumber) {
return $accountNumber;
if ($isTestMode) {
// Use test billing number for sandbox mode
$testBillingNumber = '33333333330102';
Log::info('Using DHL test billing number (sandbox mode)', [
'product_code' => $productCode,
'billing_number' => $testBillingNumber,
'test_mode' => true
]);
return $testBillingNumber;
}
// Try to get from admin settings via Setting model
// Try to get from admin settings via Setting model first (database settings override config)
try {
$settingKey = 'dhl_account_' . strtolower($productCode);
$accountNumber = \App\Models\Setting::getContentBySlug($settingKey);
if ($accountNumber) {
Log::info('Using DHL account number from database settings', [
'product_code' => $productCode,
'account_number' => $accountNumber
]);
return $accountNumber;
}
} catch (\Exception $e) {
@ -387,6 +398,16 @@ class ShippingService
]);
}
// Try to get account number from config by product code
$accountNumber = config("dhl.account_numbers.{$productCode}");
if ($accountNumber) {
Log::info('Using DHL account number from config file', [
'product_code' => $productCode,
'account_number' => $accountNumber
]);
return $accountNumber;
}
// Fallback to default billing number
$defaultBillingNumber = config('dhl.billing_number') ?: config('dhl.account_numbers.default');
@ -435,6 +456,35 @@ class ShippingService
*/
private function createShipmentRecord(array $orderData, array $payload, array $response, ?string $shipmentNumber): DhlShipment
{
// Extract recipient data from orderData (can be modified in modal)
$consignee = $orderData['consignee'] ?? [];
// Parse name from consignee data
$fullName = trim($consignee['name'] ?? '');
$nameParts = explode(' ', $fullName, 2);
$firstname = $nameParts[0] ?? '';
$lastname = $nameParts[1] ?? '';
// If name is empty, try to get from separate fields
if (empty($firstname) && empty($lastname)) {
$firstname = $consignee['firstname'] ?? '';
$lastname = $consignee['lastname'] ?? '';
}
// Prepare complete recipient address as JSON
$recipientData = [
'firstname' => $firstname,
'lastname' => $lastname,
'company' => $consignee['name2'] ?? '',
'street' => $consignee['street'] ?? '',
'houseNumber' => $consignee['houseNumber'] ?? '',
'postalCode' => $consignee['postalCode'] ?? '',
'city' => $consignee['city'] ?? '',
'country' => $consignee['country'] ?? '',
'email' => $consignee['email'] ?? '',
'phone' => $consignee['phone'] ?? '',
];
return DhlShipment::create([
'order_id' => $orderData['order_id'] ?? null,
'dhl_shipment_no' => $shipmentNumber,
@ -446,7 +496,13 @@ class ShippingService
'status' => 'created',
'label_format' => $payload['shipments'][0]['print']['format'],
'label_path' => null,
'api_response_data' => $response
'api_response_data' => $response,
// Recipient data (can be modified in modal)
'firstname' => $firstname,
'lastname' => $lastname,
'company' => $consignee['name2'] ?? '',
'recipient' => $recipientData
]);
}

View file

@ -2,14 +2,14 @@
namespace Acme\Dhl\Services;
use Acme\Dhl\Support\DhlClient;
use Acme\Dhl\Jobs\SyncTrackingJob;
use Acme\Dhl\Models\DhlShipment;
use Acme\Dhl\Models\DhlTrackingEvent;
use InvalidArgumentException;
use Acme\Dhl\Support\DhlClient;
use Exception;
use Acme\Dhl\Jobs\SyncTrackingJob;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
/**
* DHL Tracking Service for fetching and managing tracking information
@ -19,10 +19,11 @@ class TrackingService
public function __construct(protected DhlClient $client) {}
/**
* Fetch tracking status for a shipment
* Fetch tracking status for a shipment using proper DHL APIs
*
* @param string $trackingNumber DHL tracking number
* @param string $trackingNumber DHL tracking number
* @return array Tracking data from DHL API
*
* @throws InvalidArgumentException When tracking number is empty
* @throws Exception When API request fails
*/
@ -31,28 +32,47 @@ class TrackingService
if (empty($trackingNumber)) {
throw new InvalidArgumentException('Tracking number is required');
}
if (config('dhl.use_queue')) {
SyncTrackingJob::dispatch($trackingNumber);
return ['queued' => true];
}
$response = $this->client->request('get', '/post-tracking/api/shipments', [], [
'trackingNumber' => $trackingNumber
]);
Log::info('Fetched tracking status', ['trackingNumber' => $trackingNumber]);
// Use the app's DhlTrackingService for actual API calls
$trackingService = app(\App\Services\DhlTrackingService::class);
$result = $trackingService->trackShipment($trackingNumber);
$events = data_get($response, 'shipments.0.events', []);
$shipment = $this->findOrCreateShipment($trackingNumber);
if ($result['success']) {
Log::info('Fetched tracking status', [
'trackingNumber' => $trackingNumber,
'api_used' => $result['api_used'] ?? 'unknown',
]);
$this->updateTrackingEvents($shipment, $events);
// Update local database
$shipment = $this->findOrCreateShipment($trackingNumber);
return $response;
if (isset($result['events'])) {
$this->updateTrackingEvents($shipment, $result['events']);
}
// Update shipment status
$shipment->update([
'status' => $this->mapDhlStatusToInternal($result['status']),
'tracking_status' => $result['status_text'],
'last_tracked_at' => now(),
]);
return $result;
} else {
throw new Exception($result['message'] ?? 'Failed to fetch tracking status');
}
}
/**
* Update tracking status for multiple shipments
*
* @param array $trackingNumbers Array of tracking numbers to update
* @param array $trackingNumbers Array of tracking numbers to update
* @return array Results for each tracking number
*/
public function updateMultipleStatus(array $trackingNumbers): array
@ -74,14 +94,14 @@ class TrackingService
/**
* Get latest tracking status for a shipment
*
* @param string $trackingNumber DHL tracking number
* @param string $trackingNumber DHL tracking number
* @return array|null Latest tracking event or null if not found
*/
public function getLatestStatus(string $trackingNumber): ?array
{
$shipment = DhlShipment::where('dhl_shipment_no', $trackingNumber)->first();
if (!$shipment) {
if (! $shipment) {
return null;
}
@ -102,7 +122,7 @@ class TrackingService
['dhl_shipment_no' => $trackingNumber],
[
'type' => 'outbound',
'status' => 'unknown'
'status' => 'unknown',
]
);
});
@ -130,12 +150,12 @@ class TrackingService
[
'shipment_id' => $shipment->id,
'status_code' => $eventData['statusCode'] ?? null,
'event_time' => $eventData['timestamp'] ?? null
'event_time' => $eventData['timestamp'] ?? null,
],
[
'status_text' => $eventData['status'] ?? null,
'location' => $this->extractLocation($eventData),
'raw' => $eventData
'raw' => $eventData,
]
);
}
@ -173,8 +193,26 @@ class TrackingService
$shipment->update([
'status' => $status,
'tracking_status' => $latestEvent['status'] ?? null,
'last_tracked_at' => now()
'last_tracked_at' => now(),
]);
Log::info('Updated shipment status', ['shipmentId' => $shipment->id, 'newStatus' => $status]);
}
/**
* Map DHL status codes to internal status
*/
private function mapDhlStatusToInternal(string $dhlStatus): string
{
$statusMap = [
'pre-transit' => 'created',
'transit' => 'in_transit',
'out-for-delivery' => 'out_for_delivery',
'delivered' => 'delivered',
'failure' => 'failed',
'returned' => 'returned',
'exception' => 'exception',
];
return $statusMap[$dhlStatus] ?? 'unknown';
}
}

View file

@ -23,6 +23,14 @@ class DhlClient
protected ?string $password
) {}
/**
* Get DHL-specific logger
*/
private function getDhlLogger()
{
return Log::channel('dhl');
}
/**
* Make HTTP request to DHL API
*
@ -39,6 +47,20 @@ class DhlClient
$request = Http::baseUrl($this->baseUrl)
->withHeaders($this->buildHeaders())
->timeout(30)
->withOptions([
'verify' => config('dhl.ssl.verify_peer', true), // SSL certificate verification
'http_errors' => false, // Don't throw exceptions on HTTP error codes
'curl' => [
CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true),
CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0,
CURLOPT_SSLVERSION => $this->getSslVersion(),
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10),
CURLOPT_TIMEOUT => config('dhl.ssl.timeout', 30),
CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0',
]
])
->retry(3, 300, function ($exception, $attempt) {
if ($exception instanceof RequestException && $exception->response->status() === 429) {
$delay = min(1000000 * $attempt, 10000000); // Max 10 seconds
@ -67,7 +89,7 @@ class DhlClient
if ($response->failed()) {
// Log additional debug info for 400 errors
if ($response->status() === 400) {
Log::error('[DHL API] HTTP 400 Bad Request Details', [
$this->getDhlLogger()->error('[DHL API] HTTP 400 Bad Request Details', [
'method' => $method,
'uri' => $uri,
'request_payload' => $payload,
@ -83,10 +105,10 @@ class DhlClient
return $response->json() ?? [];
} catch (RequestException $e) {
Log::error('DHL API request failed', ['error' => $e->getMessage(), 'method' => $method, 'uri' => $uri]);
$this->getDhlLogger()->error('DHL API request failed', ['error' => $e->getMessage(), 'method' => $method, 'uri' => $uri]);
throw new DhlApiException("DHL API request failed: {$e->getMessage()}", $e->getCode(), $e);
} catch (Exception $e) {
Log::error('DHL API error', ['error' => $e->getMessage()]);
$this->getDhlLogger()->error('DHL API error', ['error' => $e->getMessage()]);
// Re-throw our own exceptions
if ($e instanceof DhlApiException) {
throw $e;
@ -152,6 +174,53 @@ class DhlClient
?? null;
}
/**
* Get SSL version constant based on configuration
*/
private function getSslVersion(): int
{
$sslVersion = config('dhl.ssl.ssl_version', 'TLSv1_2');
return match ($sslVersion) {
'TLSv1_0' => CURL_SSLVERSION_TLSv1_0,
'TLSv1_1' => CURL_SSLVERSION_TLSv1_1,
'TLSv1_2' => CURL_SSLVERSION_TLSv1_2,
'TLSv1_3' => defined('CURL_SSLVERSION_TLSv1_3') ? CURL_SSLVERSION_TLSv1_3 : CURL_SSLVERSION_TLSv1_2,
default => CURL_SSLVERSION_TLSv1_2,
};
}
/**
* Log detailed server environment information for debugging
*/
public function logServerEnvironment(): void
{
$info = [
'php_version' => PHP_VERSION,
'curl_version' => curl_version(),
'openssl_version' => OPENSSL_VERSION_TEXT ?? 'Unknown',
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
'dhl_config' => [
'base_url' => $this->baseUrl,
'has_api_key' => !empty($this->apiKey),
'has_username' => !empty($this->username),
'has_password' => !empty($this->password),
'ssl_verify_peer' => config('dhl.ssl.verify_peer', true),
'ssl_verify_host' => config('dhl.ssl.verify_host', true),
'ssl_version' => config('dhl.ssl.ssl_version', 'TLSv1_2'),
'timeout' => config('dhl.ssl.timeout', 30),
'connect_timeout' => config('dhl.ssl.connect_timeout', 10),
],
'environment' => [
'APP_ENV' => config('app.env'),
'APP_DEBUG' => config('app.debug'),
'APP_URL' => config('app.url'),
]
];
$this->getDhlLogger()->info('DHL Server Environment Debug Info', $info);
}
/**
* Test connection to DHL API
*
@ -159,40 +228,370 @@ class DhlClient
*/
public function testConnection(): bool
{
try {
// Check basic connectivity and authentication
// Use the simplest possible endpoint to minimize permission issues
$response = Http::baseUrl($this->baseUrl)
->withHeaders($this->buildHeaders())
->withBasicAuth($this->username, $this->password)
->timeout(10)
->get('/');
// Log server environment for debugging
$this->logServerEnvironment();
// If we get any response (even 404), the connection and auth are working
if ($response->status() === 401) {
Log::error('DHL API authentication failed: Invalid username/password');
return false;
// Try multiple connection methods for better compatibility
$methods = [
'method1' => 'Laravel HTTP with enhanced SSL',
'method2' => 'Laravel HTTP with relaxed SSL',
'method3' => 'Direct cURL fallback'
];
foreach ($methods as $methodKey => $methodName) {
try {
$this->getDhlLogger()->info("DHL API connection test - trying {$methodName}", [
'method' => $methodKey,
'base_url' => $this->baseUrl
]);
$success = $this->testConnectionWithMethod($methodKey);
if ($success) {
$this->getDhlLogger()->info("DHL API connection test successful with {$methodName}", [
'method' => $methodKey,
'base_url' => $this->baseUrl
]);
return true;
}
} catch (Exception $e) {
$this->getDhlLogger()->warning("DHL API connection test failed with {$methodName}", [
'method' => $methodKey,
'error' => $e->getMessage(),
'base_url' => $this->baseUrl
]);
}
}
if ($response->status() === 403 && str_contains($response->body(), 'api-key')) {
Log::error('DHL API authentication failed: Invalid API key');
$this->getDhlLogger()->error('DHL API connection test failed with all methods', [
'base_url' => $this->baseUrl,
'tried_methods' => array_keys($methods)
]);
return false;
}
/**
* Test connection with specific method
*/
private function testConnectionWithMethod(string $method): bool
{
switch ($method) {
case 'method1':
return $this->testConnectionEnhanced();
case 'method2':
return $this->testConnectionRelaxed();
case 'method3':
return $this->testConnectionCurl();
default:
return false;
}
// Any other response code (including 404, 403 for endpoint access) means connection works
Log::info('DHL API connection test successful', [
'status' => $response->status(),
'has_api_key' => !empty($this->apiKey),
'has_credentials' => !empty($this->username) && !empty($this->password)
]);
return true;
} catch (Exception $e) {
Log::error('DHL API connection test failed', [
'error' => $e->getMessage(),
'base_url' => $this->baseUrl
]);
return false;
}
}
/**
* Enhanced SSL connection test
*/
private function testConnectionEnhanced(): bool
{
// Detect cURL version for compatibility
$curlVersion = curl_version();
$isOldCurl = version_compare($curlVersion['version'], '8.0.0', '<');
$curlOptions = [
CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true),
CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0,
CURLOPT_SSLVERSION => $this->getSslVersion(),
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10),
CURLOPT_TIMEOUT => config('dhl.ssl.connect_timeout', 10),
CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0',
CURLOPT_VERBOSE => true, // Enable verbose output
CURLOPT_STDERR => fopen('php://temp', 'w+'), // Capture verbose output
];
// Only use HTTP/2 for newer cURL versions
if (!$isOldCurl && defined('CURL_HTTP_VERSION_2_0')) {
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
} else {
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
}
// Additional options for older cURL versions
if ($isOldCurl) {
$curlOptions[CURLOPT_TCP_NODELAY] = true;
$curlOptions[CURLOPT_TCP_KEEPALIVE] = 1;
$curlOptions[CURLOPT_TCP_KEEPIDLE] = 10;
$curlOptions[CURLOPT_TCP_KEEPINTVL] = 1;
}
// Log complete cURL request details
$this->getDhlLogger()->info('DHL Enhanced Connection Test - Complete Request Details', [
'method' => 'Enhanced SSL',
'url' => $this->baseUrl . '/',
'headers' => $this->buildHeaders(),
'auth' => [
'username' => $this->username,
'password' => '***hidden***'
],
'curl_options' => $this->formatCurlOptions($curlOptions),
'timeout' => 10,
'ssl_config' => [
'verify_peer' => config('dhl.ssl.verify_peer', true),
'verify_host' => config('dhl.ssl.verify_host', true),
'ssl_version' => config('dhl.ssl.ssl_version', 'TLSv1_2'),
]
]);
$response = Http::baseUrl($this->baseUrl)
->withHeaders($this->buildHeaders())
->withBasicAuth($this->username, $this->password)
->timeout(10)
->withOptions([
'verify' => config('dhl.ssl.verify_peer', true),
'http_errors' => false,
'curl' => $curlOptions
])
->get('/');
// Log response details
$this->getDhlLogger()->info('DHL Enhanced Connection Test - Response Details', [
'status_code' => $response->status(),
'headers' => $response->headers(),
'body' => $response->body(),
'success' => $this->validateResponse($response)
]);
return $this->validateResponse($response);
}
/**
* Relaxed SSL connection test (fallback)
*/
private function testConnectionRelaxed(): bool
{
// Detect cURL version for compatibility
$curlVersion = curl_version();
$isOldCurl = version_compare($curlVersion['version'], '8.0.0', '<');
$curlOptions = [
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_SSLVERSION => CURL_SSLVERSION_DEFAULT,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_TIMEOUT => 15,
CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0',
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, // Force HTTP/1.1
CURLOPT_VERBOSE => true, // Enable verbose output
CURLOPT_STDERR => fopen('php://temp', 'w+'), // Capture verbose output
];
// Additional options for older cURL versions
if ($isOldCurl) {
$curlOptions[CURLOPT_TCP_NODELAY] = true;
$curlOptions[CURLOPT_TCP_KEEPALIVE] = 1;
$curlOptions[CURLOPT_TCP_KEEPIDLE] = 10;
$curlOptions[CURLOPT_TCP_KEEPINTVL] = 1;
$curlOptions[CURLOPT_FRESH_CONNECT] = true;
$curlOptions[CURLOPT_FORBID_REUSE] = true;
}
// Log complete cURL request details
$this->getDhlLogger()->info('DHL Relaxed Connection Test - Complete Request Details', [
'method' => 'Relaxed SSL',
'url' => $this->baseUrl . '/',
'headers' => $this->buildHeaders(),
'auth' => [
'username' => $this->username,
'password' => '***hidden***'
],
'curl_options' => $this->formatCurlOptions($curlOptions),
'timeout' => 15,
'ssl_config' => [
'verify_peer' => false,
'verify_host' => false,
'ssl_version' => 'DEFAULT',
]
]);
$response = Http::baseUrl($this->baseUrl)
->withHeaders($this->buildHeaders())
->withBasicAuth($this->username, $this->password)
->timeout(15)
->withOptions([
'verify' => false, // Disable SSL verification as fallback
'http_errors' => false,
'curl' => $curlOptions
])
->get('/');
// Log response details
$this->getDhlLogger()->info('DHL Relaxed Connection Test - Response Details', [
'status_code' => $response->status(),
'headers' => $response->headers(),
'body' => $response->body(),
'success' => $this->validateResponse($response)
]);
return $this->validateResponse($response);
}
/**
* Direct cURL connection test (last resort)
*/
private function testConnectionCurl(): bool
{
$ch = curl_init();
$curlOptions = [
CURLOPT_URL => $this->baseUrl . '/',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'User-Agent: acme-laravel-dhl/1.0'
],
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $this->username . ':' . $this->password,
CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true),
CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0,
CURLOPT_SSLVERSION => $this->getSslVersion(),
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_VERBOSE => true,
CURLOPT_STDERR => fopen('php://temp', 'w+'),
];
// Log complete cURL request details
$this->getDhlLogger()->info('DHL Direct cURL Connection Test - Complete Request Details', [
'method' => 'Direct cURL',
'url' => $this->baseUrl . '/',
'headers' => [
'Accept: application/json',
'User-Agent: acme-laravel-dhl/1.0'
],
'auth' => [
'username' => $this->username,
'password' => '***hidden***',
'auth_type' => 'CURLAUTH_BASIC'
],
'curl_options' => $this->formatCurlOptions($curlOptions),
'timeout' => 15,
'ssl_config' => [
'verify_peer' => config('dhl.ssl.verify_peer', true),
'verify_host' => config('dhl.ssl.verify_host', true),
'ssl_version' => config('dhl.ssl.ssl_version', 'TLSv1_2'),
]
]);
curl_setopt_array($ch, $curlOptions);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
$curlInfo = curl_getinfo($ch);
// Get verbose output
rewind($curlOptions[CURLOPT_STDERR]);
$verboseOutput = stream_get_contents($curlOptions[CURLOPT_STDERR]);
fclose($curlOptions[CURLOPT_STDERR]);
curl_close($ch);
// Log response details
$this->getDhlLogger()->info('DHL Direct cURL Connection Test - Response Details', [
'status_code' => $httpCode,
'response_body' => $response,
'curl_info' => $curlInfo,
'verbose_output' => $verboseOutput,
'curl_error' => $error,
'success' => $httpCode >= 200 && $httpCode < 500
]);
if ($error) {
throw new Exception("cURL error: {$error}");
}
// For cURL, we check HTTP status codes
return $httpCode >= 200 && $httpCode < 500; // Accept 2xx, 3xx, 4xx (but not 5xx)
}
/**
* Format cURL options for logging
*/
private function formatCurlOptions(array $curlOptions): array
{
$formatted = [];
foreach ($curlOptions as $option => $value) {
$optionName = $this->getCurlOptionName($option);
// Hide sensitive values
if (in_array($option, [CURLOPT_USERPWD, CURLOPT_STDERR])) {
$formatted[$optionName] = '***hidden***';
} elseif (is_resource($value)) {
$formatted[$optionName] = 'resource';
} elseif (is_array($value)) {
$formatted[$optionName] = $value;
} else {
$formatted[$optionName] = $value;
}
}
return $formatted;
}
/**
* Get human-readable cURL option name
*/
private function getCurlOptionName(int $option): string
{
$optionNames = [
CURLOPT_URL => 'CURLOPT_URL',
CURLOPT_RETURNTRANSFER => 'CURLOPT_RETURNTRANSFER',
CURLOPT_TIMEOUT => 'CURLOPT_TIMEOUT',
CURLOPT_CONNECTTIMEOUT => 'CURLOPT_CONNECTTIMEOUT',
CURLOPT_HTTPHEADER => 'CURLOPT_HTTPHEADER',
CURLOPT_HTTPAUTH => 'CURLOPT_HTTPAUTH',
CURLOPT_USERPWD => 'CURLOPT_USERPWD',
CURLOPT_SSL_VERIFYPEER => 'CURLOPT_SSL_VERIFYPEER',
CURLOPT_SSL_VERIFYHOST => 'CURLOPT_SSL_VERIFYHOST',
CURLOPT_SSLVERSION => 'CURLOPT_SSLVERSION',
CURLOPT_FOLLOWLOCATION => 'CURLOPT_FOLLOWLOCATION',
CURLOPT_MAXREDIRS => 'CURLOPT_MAXREDIRS',
CURLOPT_VERBOSE => 'CURLOPT_VERBOSE',
CURLOPT_STDERR => 'CURLOPT_STDERR',
CURLOPT_USERAGENT => 'CURLOPT_USERAGENT',
CURLOPT_HTTP_VERSION => 'CURLOPT_HTTP_VERSION',
CURLOPT_TCP_NODELAY => 'CURLOPT_TCP_NODELAY',
CURLOPT_TCP_KEEPALIVE => 'CURLOPT_TCP_KEEPALIVE',
CURLOPT_TCP_KEEPIDLE => 'CURLOPT_TCP_KEEPIDLE',
CURLOPT_TCP_KEEPINTVL => 'CURLOPT_TCP_KEEPINTVL',
CURLOPT_FRESH_CONNECT => 'CURLOPT_FRESH_CONNECT',
CURLOPT_FORBID_REUSE => 'CURLOPT_FORBID_REUSE',
];
return $optionNames[$option] ?? "CURLOPT_UNKNOWN_{$option}";
}
/**
* Validate HTTP response for connection test
*/
private function validateResponse($response): bool
{
if ($response->status() === 401) {
$this->getDhlLogger()->error('DHL API authentication failed: Invalid username/password');
return false;
}
if ($response->status() === 403 && str_contains($response->body(), 'api-key')) {
$this->getDhlLogger()->error('DHL API authentication failed: Invalid API key');
return false;
}
// Any other response code (including 404, 403 for endpoint access) means connection works
return true;
}
}