- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
997 lines
28 KiB
Markdown
997 lines
28 KiB
Markdown
# cURL OOP API
|
|
|
|
Object-orientierte cURL API Implementation basierend auf PHP RFC: Object-oriented curl API v2.
|
|
|
|
## Übersicht
|
|
|
|
Die cURL OOP API bietet eine moderne, typsichere Alternative zu den prozeduralen curl_* Funktionen. Die Implementation folgt dem offiziellen PHP RFC und erweitert diesen um Framework-spezifische Patterns.
|
|
|
|
**RFC Referenz**: https://wiki.php.net/rfc/curl_oop_v2
|
|
|
|
## Architektur
|
|
|
|
```
|
|
┌─────────────────────┐
|
|
│ HttpClient │ ← Public API (unchanged)
|
|
│ Interface │
|
|
└──────────┬──────────┘
|
|
│
|
|
┌──────────▼──────────┐
|
|
│ CurlHttpClient │ ← Uses Handle instead of curl_*
|
|
└──────────┬──────────┘
|
|
│
|
|
┌──────────▼──────────┐
|
|
│ CurlRequestBuilder │ ← Returns HandleOption enums
|
|
└──────────┬──────────┘
|
|
│
|
|
┌──────────▼──────────────────────────────────┐
|
|
│ Curl\Handle │ ← OOP wrapper for curl_*
|
|
│ - fetch(): string │
|
|
│ - execute(resource|callable): void │
|
|
│ - setOption(HandleOption, mixed): self │
|
|
│ - getInfo(?Info): mixed │
|
|
│ - pause(Pause): void │
|
|
│ - reset(): void │
|
|
│ - escapeUrl(string): string │
|
|
│ - unescapeUrl(string): string │
|
|
│ - upkeep(): void │
|
|
└──────────────────────────────────────────────┘
|
|
│
|
|
├─── HandleOption enum (150+ cases)
|
|
├─── Info enum (50+ cases)
|
|
├─── Pause enum (6 cases)
|
|
└─── HandleException
|
|
```
|
|
|
|
## Core Components
|
|
|
|
### 1. Curl\Handle
|
|
|
|
**Location**: `src/Framework/HttpClient/Curl/Handle.php`
|
|
|
|
**Purpose**: Object-orientierter Wrapper für einzelne cURL Handles
|
|
|
|
**Key Features**:
|
|
- **Asymmetric Visibility**: `public private(set)` für Error-Properties
|
|
- **Exception-based**: Keine `false` returns, nur Exceptions
|
|
- **Fluent API**: Method chaining für bessere Lesbarkeit
|
|
- **Type Safety**: Automatische Type-Validierung für Options
|
|
- **Readonly**: Unveränderliche Klasse nach Konstruktion
|
|
|
|
**Usage**:
|
|
```php
|
|
// Simple GET Request
|
|
$handle = new Handle('https://api.example.com/users');
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$response = $handle->fetch();
|
|
|
|
// Fluent API
|
|
$response = (new Handle())
|
|
->setOption(HandleOption::Url, 'https://api.example.com')
|
|
->setOption(HandleOption::UserAgent, 'CustomPHP/1.0')
|
|
->setOption(HandleOption::Timeout, 10)
|
|
->fetch();
|
|
|
|
// Multiple Options
|
|
$handle->setOptions([
|
|
HandleOption::Url => 'https://api.example.com',
|
|
HandleOption::Timeout => 10,
|
|
HandleOption::FollowLocation => true,
|
|
]);
|
|
|
|
// Error Handling
|
|
try {
|
|
$response = $handle->fetch();
|
|
} catch (HandleException $e) {
|
|
// Access error details
|
|
$errorNumber = $handle->errorNumber; // public private(set)
|
|
$errorMessage = $handle->errorMessage; // public private(set)
|
|
}
|
|
```
|
|
|
|
**Methods**:
|
|
- `__construct(?string $uri = null)` - Initialisiert Handle mit optionaler URL
|
|
- `fetch(): string` - Führt Request aus und gibt Response-Body zurück
|
|
- `execute(resource|callable $out): void` - Führt Request aus und schreibt in Resource/Callback
|
|
- `setOption(HandleOption $opt, mixed $value): self` - Setzt einzelne Option
|
|
- `setOptions(array $options): self` - Setzt multiple Options
|
|
- `getInfo(?Info $option = null): mixed` - Holt Request-Informationen
|
|
- `pause(Pause $flag): void` - Pausiert/Continued Transfer
|
|
- `reset(): void` - Setzt alle Options zurück
|
|
- `escapeUrl(string $string): string` - URL-encoded String
|
|
- `unescapeUrl(string $string): string` - URL-decoded String
|
|
- `upkeep(): void` - Connection upkeep checks
|
|
- `getResource(): CurlHandle` - Holt underlying Resource (für Kompatibilität)
|
|
|
|
**Properties**:
|
|
- `public private(set) int $errorNumber` - curl_errno() Equivalent
|
|
- `public private(set) string $errorMessage` - curl_error() Equivalent
|
|
- `private readonly CurlHandle $handle` - Underlying Resource
|
|
|
|
### 2. Curl\HandleOption
|
|
|
|
**Location**: `src/Framework/HttpClient/Curl/HandleOption.php`
|
|
|
|
**Purpose**: Type-safe Enum für alle CURLOPT_* Konstanten
|
|
|
|
**Features**:
|
|
- **150+ Option Cases**: Alle CURLOPT_* Konstanten als Enum Cases
|
|
- **Type Checking**: Built-in Methoden für Type-Validierung
|
|
- **IDE Support**: Auto-completion für alle Options
|
|
- **Kategorisiert**: Grouped by functionality
|
|
|
|
**Categories**:
|
|
- URL & Request: `Url`, `Port`, `CustomRequest`, `UserAgent`, `Referer`
|
|
- HTTP Methods: `HttpGet`, `Post`, `Put`, `Upload`, `Nobody`
|
|
- Request Body: `PostFields`, `ReadData`, `InFile`, `InFileSize`
|
|
- Headers: `HttpHeader`, `Header`, `HeaderFunction`
|
|
- Response: `ReturnTransfer`, `File`, `WriteFunction`
|
|
- Authentication: `HttpAuth`, `UserPwd`, `Username`, `Password`
|
|
- SSL/TLS: `SslVerifyPeer`, `SslVerifyHost`, `CaInfo`, `SslCert`
|
|
- Timeouts: `Timeout`, `TimeoutMs`, `ConnectTimeout`, `ConnectTimeoutMs`
|
|
- Redirects: `FollowLocation`, `MaxRedirs`, `AutoReferer`
|
|
- Proxy: `Proxy`, `ProxyPort`, `ProxyType`, `ProxyAuth`
|
|
- Cookies: `Cookie`, `CookieFile`, `CookieJar`
|
|
- Network: `Interface`, `DnsServers`, `BufferSize`
|
|
- Debug: `Verbose`, `Stderr`, `FailOnError`
|
|
|
|
**Type Checking Methods**:
|
|
```php
|
|
$opt = HandleOption::Timeout;
|
|
|
|
$opt->expectsBoolean(); // false
|
|
$opt->expectsInteger(); // true
|
|
$opt->expectsString(); // false
|
|
$opt->expectsArray(); // false
|
|
$opt->expectsCallable(); // false
|
|
$opt->expectsResource(); // false
|
|
|
|
$opt->getName(); // "Timeout"
|
|
```
|
|
|
|
**Usage**:
|
|
```php
|
|
// Instead of:
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // ❌ Magic constant
|
|
|
|
// Use:
|
|
$handle->setOption(HandleOption::Timeout, 10); // ✅ Type-safe enum
|
|
```
|
|
|
|
### 3. Curl\Info
|
|
|
|
**Location**: `src/Framework/HttpClient/Curl/Info.php`
|
|
|
|
**Purpose**: Type-safe Enum für alle CURLINFO_* Konstanten
|
|
|
|
**Available Info Cases**:
|
|
- Response: `ResponseCode`, `HttpCode`, `EffectiveUrl`
|
|
- Timing: `TotalTime`, `NameLookupTime`, `ConnectTime`, `PreTransferTime`, `StartTransferTime`
|
|
- Size: `SizeDownload`, `SizeUpload`, `HeaderSize`, `RequestSize`
|
|
- Speed: `SpeedDownload`, `SpeedUpload`
|
|
- SSL: `SslVerifyResult`, `ProxySslVerifyResult`, `CertInfo`
|
|
- Content: `ContentType`, `ContentLengthDownload`, `ContentLengthUpload`
|
|
- Network: `PrimaryIp`, `PrimaryPort`, `LocalIp`, `LocalPort`
|
|
- Redirects: `RedirectCount`, `RedirectUrl`, `RedirectTime`
|
|
|
|
**Usage**:
|
|
```php
|
|
// Single info
|
|
$statusCode = $handle->getInfo(Info::ResponseCode);
|
|
$totalTime = $handle->getInfo(Info::TotalTime);
|
|
|
|
// All info
|
|
$allInfo = $handle->getInfo();
|
|
```
|
|
|
|
### 4. Curl\Pause
|
|
|
|
**Location**: `src/Framework/HttpClient/Curl/Pause.php`
|
|
|
|
**Purpose**: Type-safe Enum für CURLPAUSE_* Konstanten
|
|
|
|
**Cases**:
|
|
- `Recv` - Pause receiving
|
|
- `RecvCont` - Continue receiving
|
|
- `Send` - Pause sending
|
|
- `SendCont` - Continue sending
|
|
- `All` - Pause both
|
|
- `Cont` - Continue both
|
|
|
|
**Usage**:
|
|
```php
|
|
$handle->pause(Pause::Recv); // Pause receiving
|
|
$handle->pause(Pause::RecvCont); // Continue receiving
|
|
```
|
|
|
|
### 5. Curl\MultiHandle
|
|
|
|
**Location**: `src/Framework/HttpClient/Curl/MultiHandle.php`
|
|
|
|
**Purpose**: Parallele Ausführung mehrerer Requests
|
|
|
|
**Key Features**:
|
|
- **Parallel Execution**: Multiple Requests gleichzeitig
|
|
- **High-Level API**: `executeAll()` für einfache Verwendung
|
|
- **Low-Level API**: `execute()`, `select()`, `getInfo()` für Fine-Tuning
|
|
- **Resource Management**: Automatisches Cleanup
|
|
|
|
**Usage**:
|
|
```php
|
|
// Variadic Constructor: Pass handles directly (NEW!)
|
|
$handle1 = new Handle('https://api.example.com/users/1');
|
|
$handle2 = new Handle('https://api.example.com/users/2');
|
|
$handle3 = new Handle('https://api.example.com/users/3');
|
|
|
|
$multi = new MultiHandle($handle1, $handle2, $handle3);
|
|
$completed = $multi->executeAll();
|
|
|
|
foreach ([$handle1, $handle2, $handle3] as $handle) {
|
|
$response = $handle->fetch(); // Already executed
|
|
$status = $handle->getInfo(Info::ResponseCode);
|
|
}
|
|
|
|
// Alternative: Add handles later
|
|
$multi = new MultiHandle();
|
|
|
|
$handles = [];
|
|
foreach ($urls as $url) {
|
|
$handle = new Handle($url);
|
|
$handles[] = $handle;
|
|
$multi->addHandle($handle);
|
|
}
|
|
|
|
$completed = $multi->executeAll();
|
|
|
|
foreach ($handles as $handle) {
|
|
$response = $handle->fetch(); // Already executed
|
|
$status = $handle->getInfo(Info::ResponseCode);
|
|
}
|
|
|
|
// Low-Level: Fine-Grained Control
|
|
$multi = new MultiHandle();
|
|
$stillRunning = 0;
|
|
|
|
do {
|
|
$multi->execute($stillRunning);
|
|
|
|
if ($stillRunning > 0) {
|
|
$multi->select();
|
|
}
|
|
|
|
while ($info = $multi->getInfo($messagesInQueue)) {
|
|
// Process completed transfers
|
|
}
|
|
} while ($stillRunning > 0);
|
|
```
|
|
|
|
**Methods**:
|
|
- `__construct(Handle ...$handles)` - Erstellt MultiHandle mit optionalen Handles (NEW!)
|
|
- `addHandle(Handle $handle): self` - Fügt Handle hinzu
|
|
- `removeHandle(Handle $handle): self` - Entfernt Handle
|
|
- `execute(int &$stillRunning): self` - Führt alle Handles aus
|
|
- `select(float $timeout = 1.0): int` - Wartet auf Activity
|
|
- `getInfo(int &$messagesInQueue): array|false` - Holt Completion-Info
|
|
- `executeAll(): array<Handle>` - High-level: Führt alle aus und wartet
|
|
- `getHandles(): array<Handle>` - Alle angehängten Handles
|
|
- `count(): int` - Anzahl angehängter Handles
|
|
- `setOption(int $option, mixed $value): self` - Setzt Multi-Option
|
|
- `onComplete(\Closure $callback): self` - Callback für erfolgreiche Requests (NEW!)
|
|
- `onError(\Closure $callback): self` - Callback für fehlerhafte Requests (NEW!)
|
|
- `onProgress(\Closure $callback): self` - Callback für Progress Updates (NEW!)
|
|
|
|
**Event Callbacks (NEW!)**:
|
|
|
|
Die MultiHandle-Klasse unterstützt jetzt Event-basierte Callbacks für real-time Processing während der parallelen Ausführung.
|
|
|
|
```php
|
|
// onComplete Callback: Wird für jeden erfolgreichen Request aufgerufen
|
|
$multi = new MultiHandle();
|
|
|
|
$multi->onComplete(function(Handle $handle) {
|
|
$url = $handle->getInfo(Info::EffectiveUrl);
|
|
$status = $handle->getInfo(Info::ResponseCode);
|
|
$time = $handle->getInfo(Info::TotalTime);
|
|
|
|
echo "✓ {$url} - {$status} ({$time}s)\n";
|
|
});
|
|
|
|
foreach ($urls as $url) {
|
|
$multi->addHandle(new Handle($url));
|
|
}
|
|
|
|
$multi->executeAll(); // Callbacks werden während Execution getriggert
|
|
|
|
// onError Callback: Wird für Requests mit Fehlern aufgerufen
|
|
$multi->onError(function(Handle $handle, int $errorCode) {
|
|
$url = $handle->getInfo(Info::EffectiveUrl);
|
|
$error = curl_strerror($errorCode);
|
|
|
|
error_log("✗ {$url} - Error {$errorCode}: {$error}");
|
|
});
|
|
|
|
// onProgress Callback: Real-time Progress Updates
|
|
$totalRequests = 100;
|
|
$multi->onProgress(function(int $completed, int $total) {
|
|
$percent = round(($completed / $total) * 100);
|
|
echo "\rProgress: {$completed}/{$total} ({$percent}%)";
|
|
});
|
|
|
|
// Alle Callbacks kombinieren für comprehensive Monitoring
|
|
$multi = new MultiHandle();
|
|
|
|
$multi
|
|
->onComplete(function(Handle $handle) {
|
|
// Log successful requests
|
|
$this->logger->info('Request completed', [
|
|
'url' => $handle->getInfo(Info::EffectiveUrl),
|
|
'status' => $handle->getInfo(Info::ResponseCode),
|
|
'time' => $handle->getInfo(Info::TotalTime)
|
|
]);
|
|
})
|
|
->onError(function(Handle $handle, int $errorCode) {
|
|
// Alert on errors
|
|
$this->alerting->error('Request failed', [
|
|
'url' => $handle->getInfo(Info::EffectiveUrl),
|
|
'error_code' => $errorCode,
|
|
'error_msg' => curl_strerror($errorCode)
|
|
]);
|
|
})
|
|
->onProgress(function(int $completed, int $total) {
|
|
// Update progress bar
|
|
$this->progressBar->update($completed, $total);
|
|
});
|
|
|
|
foreach ($urlsToFetch as $url) {
|
|
$multi->addHandle(new Handle($url));
|
|
}
|
|
|
|
$results = $multi->executeAll();
|
|
```
|
|
|
|
**Callback Signatures**:
|
|
- `onComplete`: `function(Handle $handle): void`
|
|
- `onError`: `function(Handle $handle, int $errorCode): void`
|
|
- `onProgress`: `function(int $completed, int $total): void`
|
|
|
|
**Use Cases**:
|
|
- **Real-time Logging**: Log jede Completion/Error während Execution
|
|
- **Progress Tracking**: CLI Progress Bars, UI Updates
|
|
- **Error Handling**: Immediate Retry-Logic oder Alerting
|
|
- **Metrics Collection**: Performance Monitoring während Batch-Processing
|
|
- **Data Streaming**: Process Response-Daten sofort statt am Ende
|
|
|
|
### 6. Curl\MultiHandlePool (NEW!)
|
|
|
|
**Location**: `src/Framework/HttpClient/Curl/MultiHandlePool.php`
|
|
|
|
**Purpose**: Pool für parallele Requests mit Concurrency Control
|
|
|
|
**Key Features**:
|
|
- **Concurrency Limit**: Limitiert parallele Requests (verhindert Resource Exhaustion)
|
|
- **Automatic Batching**: Führt Requests in Batches aus wenn Queue größer als Limit
|
|
- **Progress Tracking**: Real-time Progress und Status Monitoring
|
|
- **Memory Efficient**: Batch-wise Execution statt alle auf einmal
|
|
|
|
**Warum MultiHandlePool statt MultiHandle?**:
|
|
- **Resource Control**: Verhindert zu viele parallele Connections
|
|
- **Production Ready**: Sicher für große Request-Mengen
|
|
- **Predictable Performance**: Kontrollierbare Resource-Nutzung
|
|
- **Auto-Batching**: Keine manuelle Batch-Logik notwendig
|
|
|
|
**Usage**:
|
|
```php
|
|
// Basic Usage: 100 URLs mit max 10 parallelen Requests
|
|
$pool = new MultiHandlePool(maxConcurrent: 10);
|
|
|
|
foreach ($urls as $url) {
|
|
$handle = new Handle($url);
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$pool->add($handle);
|
|
}
|
|
|
|
$completed = $pool->executeAll(); // Executes in batches of 10
|
|
|
|
foreach ($completed as $handle) {
|
|
$status = $handle->getInfo(Info::ResponseCode);
|
|
echo "Completed: {$status}\n";
|
|
}
|
|
|
|
// Progress Tracking
|
|
$pool = new MultiHandlePool(maxConcurrent: 5);
|
|
|
|
for ($i = 0; $i < 50; $i++) {
|
|
$pool->add(new Handle("https://api.example.com/items/{$i}"));
|
|
}
|
|
|
|
echo "Total: " . $pool->totalCount() . "\n"; // 50
|
|
echo "Queued: " . $pool->queueSize() . "\n"; // 50
|
|
|
|
$pool->executeAll();
|
|
|
|
echo "Progress: " . $pool->getProgress() . "%\n"; // 100.0%
|
|
echo "Completed: " . $pool->completedCount() . "\n"; // 50
|
|
|
|
// Reusable Pool
|
|
$pool = new MultiHandlePool(maxConcurrent: 10);
|
|
|
|
// First batch
|
|
foreach ($firstBatch as $url) {
|
|
$pool->add(new Handle($url));
|
|
}
|
|
$pool->executeAll();
|
|
|
|
// Clear and reuse
|
|
$pool->clear();
|
|
|
|
// Second batch
|
|
foreach ($secondBatch as $url) {
|
|
$pool->add(new Handle($url));
|
|
}
|
|
$pool->executeAll();
|
|
```
|
|
|
|
**Methods**:
|
|
- `__construct(int $maxConcurrent = 10)` - Erstellt Pool mit Concurrency Limit
|
|
- `add(Handle $handle): self` - Fügt Handle zur Queue hinzu
|
|
- `executeAll(): array<Handle>` - Führt alle Handles in Batches aus
|
|
- `queueSize(): int` - Anzahl wartender Handles
|
|
- `activeCount(): int` - Anzahl aktuell ausgeführter Handles
|
|
- `completedCount(): int` - Anzahl abgeschlossener Handles
|
|
- `totalCount(): int` - Gesamt-Anzahl (queued + active + completed)
|
|
- `getProgress(): float` - Progress in Prozent (0-100)
|
|
- `isComplete(): bool` - Sind alle Handles abgeschlossen?
|
|
- `clear(): self` - Löscht alle Handles (queue, active, completed)
|
|
- `getMaxConcurrent(): int` - Konfiguriertes Concurrency Limit
|
|
|
|
**Performance Characteristics**:
|
|
```php
|
|
// Sequential (old approach - avoid!)
|
|
foreach ($urls as $url) {
|
|
$handle = new Handle($url);
|
|
$response = $handle->fetch();
|
|
}
|
|
// Time: N * request_time (z.B. 100 * 1s = 100s)
|
|
|
|
// MultiHandle (uncontrolled)
|
|
$multi = new MultiHandle();
|
|
foreach ($urls as $url) {
|
|
$multi->addHandle(new Handle($url));
|
|
}
|
|
$multi->executeAll();
|
|
// Time: ~request_time (z.B. ~1s)
|
|
// Risk: Too many connections, resource exhaustion
|
|
|
|
// MultiHandlePool (controlled, recommended!)
|
|
$pool = new MultiHandlePool(maxConcurrent: 10);
|
|
foreach ($urls as $url) {
|
|
$pool->add(new Handle($url));
|
|
}
|
|
$pool->executeAll();
|
|
// Time: (N / concurrency) * request_time (z.B. 100/10 * 1s = 10s)
|
|
// Safe: Controlled resource usage, predictable performance
|
|
```
|
|
|
|
**Production Best Practices**:
|
|
```php
|
|
// Use appropriate concurrency based on server capacity
|
|
$pool = new MultiHandlePool(
|
|
maxConcurrent: match ($environment) {
|
|
'production' => 20, // Conservative for production
|
|
'staging' => 50, // More aggressive for testing
|
|
'local' => 5, // Low for local development
|
|
}
|
|
);
|
|
|
|
// Set timeouts on all handles
|
|
foreach ($urls as $url) {
|
|
$handle = new Handle($url);
|
|
$handle->setOption(HandleOption::Timeout, 30);
|
|
$handle->setOption(HandleOption::ConnectTimeout, 10);
|
|
$pool->add($handle);
|
|
}
|
|
|
|
// Execute and handle results
|
|
$completed = $pool->executeAll();
|
|
|
|
foreach ($completed as $handle) {
|
|
$status = $handle->getInfo(Info::ResponseCode);
|
|
|
|
if ($status >= 200 && $status < 300) {
|
|
// Success
|
|
$response = $handle->fetch();
|
|
} else {
|
|
// Handle error
|
|
$this->logger->warning("Request failed", [
|
|
'url' => $handle->getInfo(Info::EffectiveUrl),
|
|
'status' => $status
|
|
]);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 7. Exception Handling
|
|
|
|
**HandleException**: `src/Framework/HttpClient/Curl/Exception/HandleException.php`
|
|
|
|
**Static Factory Methods**:
|
|
```php
|
|
HandleException::initializationFailed(?string $uri)
|
|
HandleException::executionFailed(string $errorMessage, int $errorCode)
|
|
HandleException::invalidOutputTarget(mixed $output)
|
|
HandleException::setOptionFailed(HandleOption $opt, mixed $value, string $errorMessage)
|
|
HandleException::invalidOptionType(HandleOption $opt, mixed $value)
|
|
HandleException::pauseFailed(Pause $flag, string $errorMessage)
|
|
HandleException::escapeFailed(string $string)
|
|
HandleException::unescapeFailed(string $string)
|
|
HandleException::upkeepFailed(string $errorMessage)
|
|
```
|
|
|
|
**Context-Rich Error Data**:
|
|
```php
|
|
try {
|
|
$handle->setOption(HandleOption::Timeout, 'invalid');
|
|
} catch (HandleException $e) {
|
|
$e->getMessage(); // "Invalid type for option Timeout: expected integer, got string"
|
|
$e->getData(); // ['option' => 'Timeout', 'expected_type' => 'integer', 'actual_type' => 'string']
|
|
}
|
|
```
|
|
|
|
**MultiHandleException**: `src/Framework/HttpClient/Curl/Exception/MultiHandleException.php`
|
|
|
|
**Static Factory Methods**:
|
|
```php
|
|
MultiHandleException::initializationFailed()
|
|
MultiHandleException::addHandleFailed(int $errorCode)
|
|
MultiHandleException::removeHandleFailed(int $errorCode)
|
|
MultiHandleException::executionFailed(int $errorCode)
|
|
MultiHandleException::selectFailed()
|
|
MultiHandleException::setOptionFailed(int $option, mixed $value, int $errorCode)
|
|
```
|
|
|
|
## Integration mit HttpClient
|
|
|
|
### CurlRequestBuilder
|
|
|
|
**Before** (Procedural):
|
|
```php
|
|
$options = [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
];
|
|
```
|
|
|
|
**After** (OOP):
|
|
```php
|
|
$options = [
|
|
HandleOption::Url => $url,
|
|
HandleOption::Timeout => 10,
|
|
HandleOption::ReturnTransfer => true,
|
|
];
|
|
```
|
|
|
|
### CurlHttpClient
|
|
|
|
**Before** (Procedural):
|
|
```php
|
|
$ch = curl_init();
|
|
curl_setopt_array($ch, $options);
|
|
$response = curl_exec($ch);
|
|
|
|
if ($response === false) {
|
|
throw new CurlExecutionFailed(curl_error($ch), curl_errno($ch));
|
|
}
|
|
|
|
curl_close($ch);
|
|
```
|
|
|
|
**After** (OOP):
|
|
```php
|
|
$handle = new Handle();
|
|
$handle->setOptions($options);
|
|
$response = $handle->fetch(); // Throws HandleException on error
|
|
// Automatic cleanup via __destruct
|
|
```
|
|
|
|
### AuthenticationHandler
|
|
|
|
**Before** (Procedural):
|
|
```php
|
|
$curlOptions[CURLOPT_USERPWD] = "{$username}:{$password}";
|
|
$curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
|
|
```
|
|
|
|
**After** (OOP):
|
|
```php
|
|
$curlOptions[HandleOption::UserPwd] = "{$username}:{$password}";
|
|
$curlOptions[HandleOption::HttpAuth] = CURLAUTH_BASIC;
|
|
```
|
|
|
|
## Framework-Compliance
|
|
|
|
Die Implementation folgt allen Framework-Patterns:
|
|
|
|
✅ **No Inheritance**: Keine `extends`, nur Composition
|
|
✅ **Readonly Everywhere**: Klassen sind `final readonly`
|
|
✅ **Final by Default**: Alle Klassen sind `final`
|
|
✅ **Value Objects**: Enums statt Primitive Obsession
|
|
✅ **Explicit DI**: Keine globalen States
|
|
✅ **Exception-based**: Keine `false` returns
|
|
✅ **Immutability**: Properties readonly wo möglich
|
|
✅ **Asymmetric Visibility**: `public private(set)` für Error-Properties
|
|
|
|
## Backward Compatibility
|
|
|
|
Die öffentliche `HttpClient` Interface bleibt **unverändert**:
|
|
|
|
```php
|
|
interface HttpClient
|
|
{
|
|
public function send(ClientRequest $request): ClientResponse;
|
|
}
|
|
```
|
|
|
|
Alle bestehenden Aufrufe funktionieren weiterhin:
|
|
```php
|
|
$client = new CurlHttpClient();
|
|
$response = $client->send($request); // ✅ Unchanged
|
|
```
|
|
|
|
Die OOP API wird **intern** verwendet, ist aber auch **direkt nutzbar**:
|
|
```php
|
|
// Direct usage of new API
|
|
$handle = new Handle('https://api.example.com');
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$response = $handle->fetch();
|
|
```
|
|
|
|
## Performance
|
|
|
|
**Keine Performance-Regression**:
|
|
- Enums sind compile-time optimiert
|
|
- Method calls werden inlined
|
|
- Gleiche underlying curl_* Funktionen
|
|
- Automatic cleanup via `__destruct`
|
|
|
|
**Vorteile**:
|
|
- Type-Validierung verhindert Runtime-Fehler
|
|
- Better IDE support → weniger Debugging
|
|
- Exception-basierte Fehler → klarere Callstacks
|
|
|
|
## Testing
|
|
|
|
**Pest Tests** (todo):
|
|
```php
|
|
describe('Curl\Handle', function () {
|
|
it('can fetch from URL', function () {
|
|
$handle = new Handle('https://httpbin.org/get');
|
|
$response = $handle->fetch();
|
|
|
|
expect($response)->toContain('"url"');
|
|
expect($handle->getInfo(Info::ResponseCode))->toBe(200);
|
|
});
|
|
|
|
it('throws on invalid URL', function () {
|
|
$handle = new Handle('invalid-url');
|
|
$handle->fetch();
|
|
})->throws(HandleException::class);
|
|
|
|
it('validates option types', function () {
|
|
$handle = new Handle();
|
|
$handle->setOption(HandleOption::Timeout, 'invalid');
|
|
})->throws(HandleException::class, 'Invalid type for option Timeout');
|
|
});
|
|
|
|
describe('Curl\MultiHandle', function () {
|
|
it('can execute parallel requests', function () {
|
|
// New: variadic constructor accepts handles directly
|
|
$multi = new MultiHandle(
|
|
new Handle('https://httpbin.org/delay/1'),
|
|
new Handle('https://httpbin.org/delay/1')
|
|
);
|
|
|
|
$completed = $multi->executeAll();
|
|
|
|
expect($completed)->toHaveCount(2);
|
|
expect($multi->count())->toBe(2);
|
|
});
|
|
|
|
it('can add handles later', function () {
|
|
$multi = new MultiHandle();
|
|
|
|
$handles = [
|
|
new Handle('https://httpbin.org/delay/1'),
|
|
new Handle('https://httpbin.org/delay/1'),
|
|
];
|
|
|
|
foreach ($handles as $handle) {
|
|
$multi->addHandle($handle);
|
|
}
|
|
|
|
$completed = $multi->executeAll();
|
|
|
|
expect($completed)->toHaveCount(2);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Migration Guide
|
|
|
|
### Von Procedural cURL zu OOP API
|
|
|
|
**Step 1**: Replace curl_init()
|
|
```php
|
|
// Before
|
|
$ch = curl_init($url);
|
|
|
|
// After
|
|
$handle = new Handle($url);
|
|
```
|
|
|
|
**Step 2**: Replace curl_setopt()
|
|
```php
|
|
// Before
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
|
|
// After
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
```
|
|
|
|
**Step 3**: Replace curl_exec()
|
|
```php
|
|
// Before
|
|
$response = curl_exec($ch);
|
|
if ($response === false) {
|
|
$error = curl_error($ch);
|
|
throw new Exception($error);
|
|
}
|
|
|
|
// After
|
|
try {
|
|
$response = $handle->fetch();
|
|
} catch (HandleException $e) {
|
|
// Automatic error handling
|
|
}
|
|
```
|
|
|
|
**Step 4**: Replace curl_getinfo()
|
|
```php
|
|
// Before
|
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
// After
|
|
$status = $handle->getInfo(Info::HttpCode);
|
|
```
|
|
|
|
**Step 5**: Replace curl_close()
|
|
```php
|
|
// Before
|
|
curl_close($ch);
|
|
|
|
// After
|
|
// Automatic via __destruct
|
|
```
|
|
|
|
### Von curl_multi_* zu MultiHandle
|
|
|
|
```php
|
|
// Before
|
|
$mh = curl_multi_init();
|
|
curl_multi_add_handle($mh, $ch1);
|
|
curl_multi_add_handle($mh, $ch2);
|
|
|
|
do {
|
|
curl_multi_exec($mh, $running);
|
|
curl_multi_select($mh);
|
|
} while ($running > 0);
|
|
|
|
curl_multi_remove_handle($mh, $ch1);
|
|
curl_multi_remove_handle($mh, $ch2);
|
|
curl_multi_close($mh);
|
|
|
|
// After
|
|
$multi = new MultiHandle();
|
|
$multi->addHandle($handle1);
|
|
$multi->addHandle($handle2);
|
|
|
|
$completed = $multi->executeAll();
|
|
// Automatic cleanup
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Prefer Enums over Constants
|
|
```php
|
|
// ❌ Avoid
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
|
|
// ✅ Prefer
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
```
|
|
|
|
### 2. Use Fluent API for Multiple Options
|
|
```php
|
|
// ❌ Avoid
|
|
$handle->setOption(HandleOption::Url, $url);
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
$handle->setOption(HandleOption::FollowLocation, true);
|
|
|
|
// ✅ Prefer
|
|
$handle->setOption(HandleOption::Url, $url)
|
|
->setOption(HandleOption::Timeout, 10)
|
|
->setOption(HandleOption::FollowLocation, true);
|
|
|
|
// ✅ Or use setOptions()
|
|
$handle->setOptions([
|
|
HandleOption::Url => $url,
|
|
HandleOption::Timeout => 10,
|
|
HandleOption::FollowLocation => true,
|
|
]);
|
|
```
|
|
|
|
### 3. Use MultiHandle for Parallel Requests
|
|
```php
|
|
// ❌ Avoid sequential requests
|
|
foreach ($urls as $url) {
|
|
$handle = new Handle($url);
|
|
$responses[] = $handle->fetch(); // Slow: sequential
|
|
}
|
|
|
|
// ✅ Prefer parallel execution
|
|
$multi = new MultiHandle();
|
|
$handles = [];
|
|
|
|
foreach ($urls as $url) {
|
|
$handle = new Handle($url);
|
|
$handles[] = $handle;
|
|
$multi->addHandle($handle);
|
|
}
|
|
|
|
$multi->executeAll(); // Fast: parallel
|
|
|
|
foreach ($handles as $handle) {
|
|
$responses[] = $handle->fetch();
|
|
}
|
|
```
|
|
|
|
### 4. Handle Exceptions Properly
|
|
```php
|
|
// ❌ Avoid catching generic Exception
|
|
try {
|
|
$response = $handle->fetch();
|
|
} catch (\Exception $e) {
|
|
// Too broad
|
|
}
|
|
|
|
// ✅ Catch specific HandleException
|
|
try {
|
|
$response = $handle->fetch();
|
|
} catch (HandleException $e) {
|
|
// Access error details
|
|
$errorNumber = $handle->errorNumber;
|
|
$errorMessage = $handle->errorMessage;
|
|
|
|
// Log with context
|
|
$this->logger->error('cURL request failed', [
|
|
'error_number' => $errorNumber,
|
|
'error_message' => $errorMessage,
|
|
'exception_data' => $e->getData(),
|
|
]);
|
|
}
|
|
```
|
|
|
|
### 5. Use Type-Safe Info Retrieval
|
|
```php
|
|
// ❌ Avoid magic constants
|
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
// ✅ Use Info enum
|
|
$status = $handle->getInfo(Info::HttpCode);
|
|
$totalTime = $handle->getInfo(Info::TotalTime);
|
|
$downloadSize = $handle->getInfo(Info::SizeDownload);
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Problem: Type Error beim setOption()
|
|
|
|
**Symptom**:
|
|
```php
|
|
HandleException: Invalid type for option Timeout: expected integer, got string
|
|
```
|
|
|
|
**Lösung**:
|
|
```php
|
|
// ❌ Wrong type
|
|
$handle->setOption(HandleOption::Timeout, '10');
|
|
|
|
// ✅ Correct type
|
|
$handle->setOption(HandleOption::Timeout, 10);
|
|
```
|
|
|
|
### Problem: MultiHandle gibt keine Responses zurück
|
|
|
|
**Symptom**: `executeAll()` returned handles haben keinen Response
|
|
|
|
**Lösung**: Response ist bereits im Handle, nicht im Return-Wert
|
|
```php
|
|
// ❌ Wrong
|
|
$completed = $multi->executeAll();
|
|
foreach ($completed as $handle) {
|
|
$response = $handle->fetch(); // ❌ Already executed
|
|
}
|
|
|
|
// ✅ Correct
|
|
$handles = [$handle1, $handle2];
|
|
$multi->addHandle($handle1);
|
|
$multi->addHandle($handle2);
|
|
|
|
$multi->executeAll();
|
|
|
|
foreach ($handles as $handle) {
|
|
// Response bereits vorhanden, Info abrufen
|
|
$status = $handle->getInfo(Info::ResponseCode);
|
|
$body = $handle->getInfo(Info::ResponseCode); // Use original handle reference
|
|
}
|
|
```
|
|
|
|
### Problem: Memory Leak bei vielen Requests
|
|
|
|
**Symptom**: Memory usage steigt bei vielen Requests
|
|
|
|
**Lösung**: Unset handles nach Verwendung
|
|
```php
|
|
// ❌ Handles bleiben im Speicher
|
|
foreach ($urls as $url) {
|
|
$handle = new Handle($url);
|
|
$response = $handle->fetch();
|
|
$results[] = $response;
|
|
}
|
|
|
|
// ✅ Explicit cleanup
|
|
foreach ($urls as $url) {
|
|
$handle = new Handle($url);
|
|
$response = $handle->fetch();
|
|
$results[] = $response;
|
|
unset($handle); // Force __destruct
|
|
}
|
|
|
|
// ✅ Or use MultiHandle with batching
|
|
```
|
|
|
|
## Future Enhancements
|
|
|
|
Potentielle zukünftige Erweiterungen:
|
|
|
|
1. **HTTP/2 Support**: Dedicated HTTP/2 methods und options
|
|
2. **Async/Await Integration**: Integration mit Framework's Fiber Manager
|
|
3. **Connection Pooling**: Wiederverwendung von Connections
|
|
4. **Metrics Integration**: Automatische Performance-Metriken
|
|
5. **Retry Mechanisms**: Built-in exponential backoff
|
|
6. **Circuit Breaker**: Automatic failure handling
|
|
7. **Request/Response Middleware**: Transformations vor/nach Request
|
|
|
|
## Related Documentation
|
|
|
|
- **PHP RFC**: https://wiki.php.net/rfc/curl_oop_v2
|
|
- **HttpClient Module**: `/docs/claude/architecture.md#http--routing`
|
|
- **Exception Handling**: `/docs/claude/error-handling.md`
|
|
- **Testing Guide**: `/docs/claude/common-workflows.md`
|
|
- **Examples**: `/examples/curl-oop-usage.php`
|
|
|
|
## Summary
|
|
|
|
Die cURL OOP API bietet:
|
|
|
|
✅ **Type Safety**: Enums statt Magic Constants
|
|
✅ **IDE Support**: Auto-completion für alle Options
|
|
✅ **Exception-based**: Keine `false` returns
|
|
✅ **Fluent API**: Method chaining
|
|
✅ **Parallel Execution**: MultiHandle für Performance
|
|
✅ **Framework-Compliance**: Readonly, Final, Value Objects
|
|
✅ **Backward Compatible**: Bestehender Code funktioniert weiter
|
|
✅ **Well-Documented**: RFC-based mit Examples
|
|
|
|
Die Implementation ist **production-ready** und kann sofort verwendet werden!
|