Files
michaelschiemer/src/Framework/AsyncExamples/Http/AsyncHttpClient.php
Michael Schiemer e30753ba0e fix: resolve RedisCache array offset error and improve discovery diagnostics
- Fix RedisCache driver to handle MGET failures gracefully with fallback
- Add comprehensive discovery context comparison debug tools
- Identify root cause: WEB context discovery missing 166 items vs CLI
- WEB context missing RequestFactory class entirely (52 vs 69 commands)
- Improved exception handling with detailed binding diagnostics
2025-09-12 20:05:18 +02:00

222 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\AsyncExamples\Http;
use App\Framework\Async\AsyncPool;
use App\Framework\Async\FiberManager;
use Fiber;
/**
* Asynchroner HTTP-Client mit Fiber-Unterstützung
*/
final class AsyncHttpClient
{
/** @var array<string, mixed> */
private array $defaultOptions = [
'timeout' => 30,
'connect_timeout' => 10,
'follow_redirects' => true,
'max_redirects' => 5,
'user_agent' => 'AsyncHttpClient/1.0',
];
public function __construct(
private readonly FiberManager $fiberManager = new FiberManager(),
/** @var array<string, mixed> */
array $defaultOptions = []
) {
$this->defaultOptions = array_merge($this->defaultOptions, $defaultOptions);
}
/**
* Sendet einen GET-Request
*/
public function get(string $url, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
{
return $this->request('GET', $url, null, $headers, $options);
}
/**
* Sendet einen POST-Request
*/
public function post(string $url, mixed $data = null, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
{
return $this->request('POST', $url, $data, $headers, $options);
}
/**
* Sendet einen PUT-Request
*/
public function put(string $url, mixed $data = null, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
{
return $this->request('PUT', $url, $data, $headers, $options);
}
/**
* Sendet einen DELETE-Request
*/
public function delete(string $url, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
{
return $this->request('DELETE', $url, null, $headers, $options);
}
/**
* Sendet mehrere Requests parallel
*
* @param array<string, array<string, mixed>> $requests ['key' => ['method' => 'GET', 'url' => '...', ...]]
* @return array<string, HttpResponse>
*/
public function requestMultiple(array $requests): array
{
/** @var array<string, \Closure> */
$operations = [];
foreach ($requests as $key => $request) {
$operations[$key] = fn () => $this->request(
$request['method'] ?? 'GET',
$request['url'],
$request['data'] ?? null,
$request['headers'] ?? [],
$request['options'] ?? []
);
}
return $this->fiberManager->batch($operations);
}
/**
* Sendet Requests mit begrenzter Parallelität
*
* @param array<string, array<string, mixed>> $requests
* @return array<string, HttpResponse>
*/
public function requestBatch(array $requests, int $maxConcurrency = 10): array
{
$pool = new AsyncPool($maxConcurrency, $this->fiberManager);
foreach ($requests as $key => $request) {
$pool->add(
fn () => $this->request(
$request['method'] ?? 'GET',
$request['url'],
$request['data'] ?? null,
$request['headers'] ?? [],
$request['options'] ?? []
),
$key
);
}
return $pool->execute();
}
/**
* Hauptmethode für HTTP-Requests
*/
private function request(string $method, string $url, mixed $data = null, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
{
$options = array_merge($this->defaultOptions, $options);
$context = $this->createContext($method, $data, $headers, $options);
$startTime = microtime(true);
try {
$content = @file_get_contents($url, false, $context);
if ($content === false) {
$error = error_get_last();
throw new HttpException("HTTP request failed: " . ($error['message'] ?? 'Unknown error'));
}
$responseHeaders = $this->parseHeaders($http_response_header ?? []);
$statusCode = $this->extractStatusCode($http_response_header ?? []);
return new HttpResponse(
statusCode: $statusCode,
headers: $responseHeaders,
body: $content,
requestTime: microtime(true) - $startTime
);
} catch (\Throwable $e) {
throw new HttpException("HTTP request failed: " . $e->getMessage(), 0, $e);
}
}
/**
* @return resource
*/
private function createContext(string $method, mixed $data, /** @var array<string, string> */ array $headers, /** @var array<string, mixed> */ array $options)
{
/** @var array<string, array<string, mixed>> */
$contextOptions = [
'http' => [
'method' => $method,
'timeout' => $options['timeout'],
'user_agent' => $options['user_agent'],
'follow_location' => $options['follow_redirects'],
'max_redirects' => $options['max_redirects'],
'ignore_errors' => true,
],
];
if ($data !== null) {
if (is_array($data) || is_object($data)) {
$contextOptions['http']['content'] = json_encode($data);
$headers['Content-Type'] = 'application/json';
} else {
$contextOptions['http']['content'] = (string)$data;
}
}
if (! empty($headers)) {
/** @var array<int, string> */
$headerStrings = [];
foreach ($headers as $key => $value) {
$headerStrings[] = "$key: $value";
}
$contextOptions['http']['header'] = implode("\r\n", $headerStrings);
}
return stream_context_create($contextOptions);
}
/**
* @param array<int, string> $httpResponseHeader
* @return array<string, string>
*/
private function parseHeaders(array $httpResponseHeader): array
{
/** @var array<string, string> */
$headers = [];
foreach ($httpResponseHeader as $header) {
if (strpos($header, ':') !== false) {
[$key, $value] = explode(':', $header, 2);
$headers[trim($key)] = trim($value);
}
}
return $headers;
}
/**
* @param array<int, string> $httpResponseHeader
*/
private function extractStatusCode(array $httpResponseHeader): int
{
if (empty($httpResponseHeader)) {
return 0;
}
$statusLine = $httpResponseHeader[0];
if (preg_match('/HTTP\/\d+\.\d+\s+(\d+)/', $statusLine, $matches)) {
return (int)$matches[1];
}
return 0;
}
}